Compare commits

..

2 Commits

Author SHA1 Message Date
Hui Wang
72eb3c6cf6 Merge branch 'master' into improve-template 2025-01-13 14:17:03 +08:00
Haley Wang
2489095595 vmalert: improve template performance 2025-01-13 14:10:23 +08:00
1020 changed files with 401042 additions and 14644 deletions

View File

@@ -567,7 +567,7 @@ golangci-lint: install-golangci-lint
golangci-lint run
install-golangci-lint:
which golangci-lint || curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(shell go env GOPATH)/bin v1.63.4
which golangci-lint || curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(shell go env GOPATH)/bin v1.60.3
remove-golangci-lint:
rm -rf `which golangci-lint`

View File

@@ -176,7 +176,7 @@ func writeCompactObject(w io.Writer, fields []logstorage.Field) error {
_, err := fmt.Fprintf(w, "%s\n", fields[0].Value)
return err
}
if len(fields) == 2 && (fields[0].Name == "_time" || fields[1].Name == "_time") {
if len(fields) == 2 && fields[0].Name == "_time" || fields[1].Name == "_time" {
// Write _time\tfieldValue as is
if fields[0].Name == "_time" {
_, err := fmt.Fprintf(w, "%s\t%s\n", fields[0].Value, fields[1].Value)

View File

@@ -688,13 +688,13 @@ func ProcessStatsQueryRangeRequest(ctx context.Context, w http.ResponseWriter, r
m := make(map[string]*statsSeries)
var mLock sync.Mutex
timestamp := q.GetTimestamp()
writeBlock := func(_ uint, timestamps []int64, columns []logstorage.BlockColumn) {
clonedColumnNames := make([]string, len(columns))
for i, c := range columns {
clonedColumnNames[i] = strings.Clone(c.Name)
}
for i := range timestamps {
timestamp := q.GetTimestamp()
labels := make([]logstorage.Field, 0, len(byFields))
for j, c := range columns {
if c.Name == "_time" {

View File

@@ -28,7 +28,7 @@ func TestParseExtraFilters_Success(t *testing.T) {
// LogsQL filter
f(`foobar`, `foobar`)
f(`foo:bar`, `foo:bar`)
f(`foo:(bar or baz) error _time:5m {"foo"=bar,baz="z"}`, `{foo="bar",baz="z"} (foo:bar or foo:baz) error _time:5m`)
f(`foo:(bar or baz) error _time:5m {"foo"=bar,baz="z"}`, `(foo:bar or foo:baz) error _time:5m {foo="bar",baz="z"}`)
}
func TestParseExtraFilters_Failure(t *testing.T) {
@@ -77,7 +77,7 @@ func TestParseExtraStreamFilters_Success(t *testing.T) {
// LogsQL filter
f(`foobar`, `foobar`)
f(`foo:bar`, `foo:bar`)
f(`foo:(bar or baz) error _time:5m {"foo"=bar,baz="z"}`, `{foo="bar",baz="z"} (foo:bar or foo:baz) error _time:5m`)
f(`foo:(bar or baz) error _time:5m {"foo"=bar,baz="z"}`, `(foo:bar or foo:baz) error _time:5m {foo="bar",baz="z"}`)
}
func TestParseExtraStreamFilters_Failure(t *testing.T) {

View File

@@ -1,12 +1,12 @@
{
"files": {
"main.css": "./static/css/main.3134e778.css",
"main.js": "./static/js/main.82cd6930.js",
"main.css": "./static/css/main.4aacd559.css",
"main.js": "./static/js/main.5ce54a05.js",
"static/js/685.f772060c.chunk.js": "./static/js/685.f772060c.chunk.js",
"index.html": "./index.html"
},
"entrypoints": [
"static/css/main.3134e778.css",
"static/js/main.82cd6930.js"
"static/css/main.4aacd559.css",
"static/js/main.5ce54a05.js"
]
}

View File

@@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="./favicon.svg"/><link rel="apple-touch-icon" href="./favicon.svg"/><link rel="mask-icon" href="./favicon.svg" color="#000000"><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=5"/><meta name="theme-color" content="#000000"/><meta name="description" content="Explore your log data with VictoriaLogs UI"/><link rel="manifest" href="./manifest.json"/><title>UI for VictoriaLogs</title><meta name="twitter:card" content="summary"><meta name="twitter:title" content="UI for VictoriaLogs"><meta name="twitter:site" content="@https://victoriametrics.com/products/victorialogs/"><meta name="twitter:description" content="Explore your log data with VictoriaLogs UI"><meta name="twitter:image" content="./preview.jpg"><meta property="og:type" content="website"><meta property="og:title" content="UI for VictoriaLogs"><meta property="og:url" content="https://victoriametrics.com/products/victorialogs/"><meta property="og:description" content="Explore your log data with VictoriaLogs UI"><script defer="defer" src="./static/js/main.82cd6930.js"></script><link href="./static/css/main.3134e778.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="./favicon.svg"/><link rel="apple-touch-icon" href="./favicon.svg"/><link rel="mask-icon" href="./favicon.svg" color="#000000"><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=5"/><meta name="theme-color" content="#000000"/><meta name="description" content="Explore your log data with VictoriaLogs UI"/><link rel="manifest" href="./manifest.json"/><title>UI for VictoriaLogs</title><meta name="twitter:card" content="summary"><meta name="twitter:title" content="UI for VictoriaLogs"><meta name="twitter:site" content="@https://victoriametrics.com/products/victorialogs/"><meta name="twitter:description" content="Explore your log data with VictoriaLogs UI"><meta name="twitter:image" content="./preview.jpg"><meta property="og:type" content="website"><meta property="og:title" content="UI for VictoriaLogs"><meta property="og:url" content="https://victoriametrics.com/products/victorialogs/"><meta property="og:description" content="Explore your log data with VictoriaLogs UI"><script defer="defer" src="./static/js/main.5ce54a05.js"></script><link href="./static/css/main.4aacd559.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -8,6 +8,8 @@ tests:
input_series:
- series: 'up{job="vmagent2", instance="localhost:9090"}'
values: "0+0x1440"
- series: "test_query"
values: "1x200"
metricsql_expr_test:
- expr: suquery_interval_test
@@ -32,7 +34,8 @@ tests:
- eval_time: 0
alertname: AlwaysFiring
exp_alerts:
- {}
- exp_annotations:
queryValue: "1"
- eval_time: 0
alertname: InstanceDown

View File

@@ -12,6 +12,8 @@ groups:
dashboard: '{{ $externalURL }}/d/dashboard?orgId=1'
- alert: AlwaysFiring
expr: 1
annotations:
queryValue: "{{ query \"test_query\" | first | value }}"
- alert: SameAlertNameWithDifferentGroup
expr: absent(test)
for: 1m

View File

@@ -10,6 +10,8 @@ tests:
input_series:
- series: "test"
values: "_x5 1x5 _ stale"
- series: "test_query"
values: "1x100"
alert_rule_test:
- eval_time: 1m
@@ -50,6 +52,8 @@ tests:
values: "0+0x1440"
- series: "test"
values: "0+1x1440"
- series: "test_query"
values: "0+1x100"
metricsql_expr_test:
- expr: count(ALERTS) by (alertgroup, alertname, alertstate)
@@ -97,6 +101,17 @@ tests:
exp_alerts:
- exp_labels:
cluster: prod
exp_annotations:
queryValue: "0"
- eval_time: 5m
groupname: group1
alertname: AlwaysFiring
exp_alerts:
- exp_labels:
cluster: prod
exp_annotations:
queryValue: "5"
- eval_time: 0
groupname: alerts

View File

@@ -8,6 +8,8 @@ tests:
input_series:
- series: 'up{job="vmagent2", instance="localhost:9090"}'
values: "0+0x1440"
- series: "test_query"
values: "0+1x200"
metricsql_expr_test:
- expr: suquery_interval_test
@@ -37,6 +39,8 @@ tests:
exp_alerts:
- exp_labels:
cluster: prod
exp_annotations:
queryValue: "0"
- eval_time: 0
groupname: group1

View File

@@ -64,7 +64,18 @@ func UnitTest(files []string, disableGroupLabel bool, externalLabels []string, e
if err != nil {
logger.Fatalf("failed to parse external URL: %w", err)
}
if err := templates.Load([]string{}, *eu); err != nil {
labels := make(map[string]string)
for _, s := range externalLabels {
if len(s) == 0 {
continue
}
n := strings.IndexByte(s, '=')
if n < 0 {
logger.Fatalf("missing '=' in `-label`. It must contain label in the form `name=value`; got %q", s)
}
labels[s[:n]] = s[n+1:]
}
if err := templates.Init([]string{}, labels, *eu); err != nil {
logger.Fatalf("failed to load template: %v", err)
}
storagePath = filepath.Join(os.TempDir(), testStoragePath)
@@ -84,19 +95,7 @@ func UnitTest(files []string, disableGroupLabel bool, externalLabels []string, e
if len(testfiles) == 0 {
logger.Fatalf("no test file found")
}
labels := make(map[string]string)
for _, s := range externalLabels {
if len(s) == 0 {
continue
}
n := strings.IndexByte(s, '=')
if n < 0 {
logger.Fatalf("missing '=' in `-label`. It must contain label in the form `name=value`; got %q", s)
}
labels[s[:n]] = s[n+1:]
}
_, err = notifier.Init(nil, labels, externalURL)
_, err = notifier.Init(nil)
if err != nil {
logger.Fatalf("failed to init notifier: %v", err)
}

View File

@@ -9,14 +9,13 @@ import (
"testing"
"time"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/notifier"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/templates"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promutils"
"gopkg.in/yaml.v2"
)
func TestMain(m *testing.M) {
if err := templates.Load([]string{"testdata/templates/*good.tmpl"}, url.URL{}); err != nil {
if err := templates.Init([]string{"testdata/templates/*good.tmpl"}, nil, url.URL{}); err != nil {
os.Exit(1)
}
os.Exit(m.Run())
@@ -79,7 +78,7 @@ groups:
for i, u := range urls {
urls[i] = srv.URL + u
}
_, err := Parse(urls, notifier.ValidateTemplates, true)
_, err := Parse(urls, templates.ValidateTemplates, true)
if err != nil && !expErr {
t.Fatalf("error parsing URLs %s", err)
}
@@ -95,7 +94,7 @@ groups:
}
func TestParse_Success(t *testing.T) {
_, err := Parse([]string{"testdata/rules/*good.rules", "testdata/dir/*good.*"}, notifier.ValidateTemplates, true)
_, err := Parse([]string{"testdata/rules/*good.rules", "testdata/dir/*good.*"}, templates.ValidateTemplates, true)
if err != nil {
t.Fatalf("error parsing files %s", err)
}
@@ -105,7 +104,7 @@ func TestParse_Failure(t *testing.T) {
f := func(paths []string, errStrExpected string) {
t.Helper()
_, err := Parse(paths, notifier.ValidateTemplates, true)
_, err := Parse(paths, templates.ValidateTemplates, true)
if err == nil {
t.Fatalf("expected to get error")
}
@@ -116,7 +115,7 @@ func TestParse_Failure(t *testing.T) {
f([]string{"testdata/rules/rules_interval_bad.rules"}, "eval_offset should be smaller than interval")
f([]string{"testdata/rules/rules0-bad.rules"}, "unexpected token")
f([]string{"testdata/dir/rules0-bad.rules"}, "error parsing annotation")
f([]string{"testdata/dir/rules0-bad.rules"}, "failed to parse text")
f([]string{"testdata/dir/rules1-bad.rules"}, "duplicate in file")
f([]string{"testdata/dir/rules2-bad.rules"}, "function \"unknown\" not defined")
f([]string{"testdata/dir/rules3-bad.rules"}, "either `record` or `alert` must be set")
@@ -330,7 +329,7 @@ func TestGroupValidate_Success(t *testing.T) {
var validateTplFn ValidateTplFn
if validateAnnotations {
validateTplFn = notifier.ValidateTemplates
validateTplFn = templates.ValidateTemplates
}
err := group.Validate(validateTplFn, validateExpressions)
if err != nil {

View File

@@ -83,7 +83,6 @@ absolute path to all .tpl files in root.
var (
alertURLGeneratorFn notifier.AlertURLGenerator
extURL *url.URL
)
func main() {
@@ -99,18 +98,29 @@ func main() {
logger.Init()
var err error
extURL, err = getExternalURL(*externalURL)
extURL, err := getExternalURL(*externalURL)
if err != nil {
logger.Fatalf("failed to init external.url %q: %s", *externalURL, err)
}
externalls := make(map[string]string)
for _, s := range *externalLabels {
if len(s) == 0 {
continue
}
n := strings.IndexByte(s, '=')
if n < 0 {
logger.Fatalf("wrong format in `-external.label`, it must contain label as `Name=value`; got %q", s)
}
externalls[s[:n]] = s[n+1:]
}
err = templates.Load(*ruleTemplatesPath, *extURL)
err = templates.Init(*ruleTemplatesPath, externalls, *extURL)
if err != nil {
logger.Fatalf("failed to load template %q: %s", *ruleTemplatesPath, err)
}
if *dryRun {
groups, err := config.Parse(*rulePath, notifier.ValidateTemplates, true)
groups, err := config.Parse(*rulePath, templates.ValidateTemplates, true)
if err != nil {
logger.Fatalf("failed to parse %q: %s", *rulePath, err)
}
@@ -127,7 +137,7 @@ func main() {
var validateTplFn config.ValidateTplFn
if *validateTemplates {
validateTplFn = notifier.ValidateTemplates
validateTplFn = templates.ValidateTemplates
}
if *replayFrom != "" {
@@ -156,7 +166,7 @@ func main() {
}
ctx, cancel := context.WithCancel(context.Background())
manager, err := newManager(ctx)
manager, err := newManager(ctx, externalls)
if err != nil {
logger.Fatalf("failed to init: %s", err)
}
@@ -203,25 +213,13 @@ var (
configTimestamp = metrics.NewCounter(`vmalert_config_last_reload_success_timestamp_seconds`)
)
func newManager(ctx context.Context) (*manager, error) {
func newManager(ctx context.Context, externalls map[string]string) (*manager, error) {
q, err := datasource.Init(nil)
if err != nil {
return nil, fmt.Errorf("failed to init datasource: %w", err)
}
labels := make(map[string]string)
for _, s := range *externalLabels {
if len(s) == 0 {
continue
}
n := strings.IndexByte(s, '=')
if n < 0 {
return nil, fmt.Errorf("missing '=' in `-label`. It must contain label in the form `Name=value`; got %q", s)
}
labels[s[:n]] = s[n+1:]
}
nts, err := notifier.Init(alertURLGeneratorFn, labels, *externalURL)
nts, err := notifier.Init(alertURLGeneratorFn)
if err != nil {
return nil, fmt.Errorf("failed to init notifier: %w", err)
}
@@ -229,7 +227,7 @@ func newManager(ctx context.Context) (*manager, error) {
groups: make(map[uint64]*rule.Group),
querierBuilder: q,
notifiers: nts,
labels: labels,
labels: externalls,
}
rw, err := remotewrite.Init(ctx)
if err != nil {
@@ -293,24 +291,36 @@ func getAlertURLGenerator(externalURL *url.URL, externalAlertSource string, vali
}, nil
}
if validateTemplate {
if err := notifier.ValidateTemplates(map[string]string{
if err := templates.ValidateTemplates(map[string]string{
"tpl": externalAlertSource,
}); err != nil {
return nil, fmt.Errorf("error validating source template %s: %w", externalAlertSource, err)
return nil, fmt.Errorf("cannot parse `external.alert.source` %q: %w", externalAlertSource, err)
}
}
m := map[string]string{
"tpl": externalAlertSource,
var err error
tmpl := templates.GetCurrentTmpl()
tmpl, err = templates.ParseWithFixedHeader(externalAlertSource, tmpl)
if err != nil {
return nil, err
}
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")
// recreate template if it was changed during config reload
cm := templates.GetCurrentTmpl()
if tmpl.Name() != cm.Name() {
tmpl = cm
tmpl, err = templates.ParseWithFixedHeader(externalAlertSource, tmpl)
if err != nil {
logger.Errorf("cannot parse `external.alert.source` %q: %w", externalAlertSource, err)
return fmt.Sprintf("%s/%s", externalURL, externalAlertSource)
}
}
templated, err := alert.ExecTemplate(qFn, alert.Labels, m)
tplData := alert.ToTplData()
rr, err := templates.ExecuteWithTemplate(tplData, tmpl)
if err != nil {
logger.Errorf("cannot template alert source: %s", err)
logger.Errorf("can not template alert source: %v", err)
return fmt.Sprintf("%s/%s", externalURL, externalAlertSource)
}
return fmt.Sprintf("%s/%s", externalURL, templated["tpl"])
return fmt.Sprintf("%s/%s", externalURL, rr)
}, nil
}
@@ -334,7 +344,7 @@ func configReload(ctx context.Context, m *manager, groupsCfg []config.Group, sig
var validateTplFn config.ValidateTplFn
if *validateTemplates {
validateTplFn = notifier.ValidateTemplates
validateTplFn = templates.ValidateTemplates
}
// init metrics for config state with positive values to improve alerting conditions
@@ -363,7 +373,7 @@ func configReload(ctx context.Context, m *manager, groupsCfg []config.Group, sig
logger.Errorf("failed to reload notifier config: %s", err)
continue
}
err := templates.Load(*ruleTemplatesPath, *extURL)
err := templates.LoadTemplateFile(*ruleTemplatesPath)
if err != nil {
setConfigError(err)
logger.Errorf("failed to load new templates: %s", err)
@@ -376,7 +386,6 @@ func configReload(ctx context.Context, m *manager, groupsCfg []config.Group, sig
continue
}
if configsEqual(newGroupsCfg, groupsCfg) {
templates.Reload()
// set success to 1 since previous reload could have been unsuccessful
// do not update configTimestamp as config version remains old.
configSuccess.Set(1)
@@ -390,7 +399,6 @@ func configReload(ctx context.Context, m *manager, groupsCfg []config.Group, sig
logger.Errorf("error while reloading rules: %s", err)
continue
}
templates.Reload()
groupsCfg = newGroupsCfg
setConfigSuccessAt(fasttime.UnixTimestamp())
logger.Infof("Rules reloaded successfully from %q", *rulePath)

View File

@@ -74,10 +74,7 @@ func TestGetAlertURLGenerator(t *testing.T) {
func TestConfigReload(t *testing.T) {
originalRulePath := *rulePath
originalExternalURL := extURL
extURL = &url.URL{}
defer func() {
extURL = originalExternalURL
*rulePath = originalRulePath
}()

View File

@@ -160,8 +160,8 @@ func (m *manager) update(ctx context.Context, groupsCfg []config.Group, restore
// it is important to call InterruptEval before the update, because cancel fn
// can be re-assigned during the update.
item.old.InterruptEval()
go func(oldGroup *rule.Group, newGroup *rule.Group) {
oldGroup.UpdateWith(newGroup)
go func(old *rule.Group, new *rule.Group) {
old.UpdateWith(new)
wg.Done()
}(item.old, item.new)
}

View File

@@ -19,7 +19,7 @@ import (
)
func TestMain(m *testing.M) {
if err := templates.Load([]string{"testdata/templates/*good.tmpl"}, url.URL{}); err != nil {
if err := templates.Init([]string{"testdata/templates/*good.tmpl"}, nil, url.URL{}); err != nil {
os.Exit(1)
}
os.Exit(m.Run())
@@ -72,7 +72,7 @@ func TestManagerUpdateConcurrent(t *testing.T) {
r := rand.New(rand.NewSource(int64(n)))
for i := 0; i < iterations; i++ {
rnd := r.Intn(len(paths))
cfg, err := config.Parse([]string{paths[rnd]}, notifier.ValidateTemplates, true)
cfg, err := config.Parse([]string{paths[rnd]}, templates.ValidateTemplates, true)
if err != nil { // update can fail and this is expected
continue
}
@@ -135,7 +135,7 @@ func TestManagerUpdate_Success(t *testing.T) {
t.Fatalf("failed to complete initial rules update: %s", err)
}
cfgUpdate, err := config.Parse([]string{updatePath}, notifier.ValidateTemplates, true)
cfgUpdate, err := config.Parse([]string{updatePath}, templates.ValidateTemplates, true)
if err == nil { // update can fail and that's expected
_ = m.update(ctx, cfgUpdate, false)
}
@@ -326,7 +326,7 @@ func loadCfg(t *testing.T, path []string, validateAnnotations, validateExpressio
t.Helper()
var validateTplFn config.ValidateTplFn
if validateAnnotations {
validateTplFn = notifier.ValidateTemplates
validateTplFn = templates.ValidateTemplates
}
cfg, err := config.Parse(path, validateTplFn, validateExpressions)
if err != nil {

View File

@@ -1,15 +1,9 @@
package notifier
import (
"bytes"
"fmt"
"io"
"strings"
textTpl "text/template"
"time"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/templates"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/utils"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promrelabel"
)
@@ -76,118 +70,18 @@ func (as AlertState) String() string {
return "inactive"
}
// AlertTplData is used to execute templating
type AlertTplData struct {
Labels map[string]string
Value float64
Expr string
AlertID uint64
GroupID uint64
ActiveAt time.Time
For time.Duration
}
var tplHeaders = []string{
"{{ $value := .Value }}",
"{{ $labels := .Labels }}",
"{{ $expr := .Expr }}",
"{{ $externalLabels := .ExternalLabels }}",
"{{ $externalURL := .ExternalURL }}",
"{{ $alertID := .AlertID }}",
"{{ $groupID := .GroupID }}",
"{{ $activeAt := .ActiveAt }}",
"{{ $for := .For }}",
}
// ExecTemplate executes the Alert template for given
// map of annotations.
// Every alert could have a different datasource, so function
// requires a queryFunction as an argument.
func (a *Alert) ExecTemplate(q templates.QueryFn, labels, annotations map[string]string) (map[string]string, error) {
tplData := AlertTplData{
// ToTplData converts Alert to AlertTplData,
// which only exposes necessary fields for template.
func (a Alert) ToTplData() templates.AlertTplData {
return templates.AlertTplData{
Value: a.Value,
Labels: labels,
Labels: a.Labels,
Expr: a.Expr,
AlertID: a.ID,
GroupID: a.GroupID,
ActiveAt: a.ActiveAt,
For: a.For,
}
return ExecTemplate(q, annotations, tplData)
}
// ExecTemplate executes the given template for given annotations map.
func ExecTemplate(q templates.QueryFn, annotations map[string]string, tplData AlertTplData) (map[string]string, error) {
tmpl, err := templates.GetWithFuncs(templates.FuncsWithQuery(q))
if err != nil {
return nil, fmt.Errorf("error cloning template: %w", err)
}
return templateAnnotations(annotations, tplData, tmpl, true)
}
// ValidateTemplates validate annotations for possible template error, uses empty data for template population
func ValidateTemplates(annotations map[string]string) error {
tmpl, err := templates.GetWithFuncs(nil)
if err != nil {
return err
}
_, err = templateAnnotations(annotations, AlertTplData{
Labels: map[string]string{},
Value: 0,
}, tmpl, false)
return err
}
func templateAnnotations(annotations map[string]string, data AlertTplData, tmpl *textTpl.Template, execute bool) (map[string]string, error) {
var builder strings.Builder
var buf bytes.Buffer
eg := new(utils.ErrGroup)
r := make(map[string]string, len(annotations))
tData := tplData{data, externalLabels, externalURL}
header := strings.Join(tplHeaders, "")
for key, text := range annotations {
// simple check to skip text without template
if !strings.Contains(text, "{{") || !strings.Contains(text, "}}") {
r[key] = text
continue
}
buf.Reset()
builder.Reset()
builder.Grow(len(header) + len(text))
builder.WriteString(header)
builder.WriteString(text)
// clone a new template for each parse to avoid collision
ctmpl, _ := tmpl.Clone()
ctmpl = ctmpl.Option("missingkey=zero")
if err := templateAnnotation(&buf, builder.String(), tData, ctmpl, execute); err != nil {
r[key] = text
eg.Add(fmt.Errorf("key %q, template %q: %w", key, text, err))
continue
}
r[key] = buf.String()
}
return r, eg.Err()
}
type tplData struct {
AlertTplData
ExternalLabels map[string]string
ExternalURL string
}
func templateAnnotation(dst io.Writer, text string, data tplData, tpl *textTpl.Template, execute bool) error {
tpl, err := tpl.Parse(text)
if err != nil {
return fmt.Errorf("error parsing annotation template: %w", err)
}
if !execute {
return nil
}
if err = tpl.Execute(dst, data); err != nil {
return fmt.Errorf("error evaluating annotation template: %w", err)
}
return nil
}
func (a Alert) applyRelabelingIfNeeded(relabelCfg *promrelabel.ParsedConfigs) []prompbmarshal.Label {

View File

@@ -1,207 +1,13 @@
package notifier
import (
"fmt"
"reflect"
"testing"
"time"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/datasource"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promrelabel"
)
func TestAlertExecTemplate(t *testing.T) {
extLabels := make(map[string]string)
const (
extCluster = "prod"
extDC = "east"
extURL = "https://foo.bar"
)
extLabels["cluster"] = extCluster
extLabels["dc"] = extDC
_, err := Init(nil, extLabels, extURL)
checkErr(t, err)
f := func(alert *Alert, annotations map[string]string, tplExpected map[string]string) {
t.Helper()
if err := ValidateTemplates(annotations); err != nil {
t.Fatalf("cannot validate annotations: %s", err)
}
qFn := func(_ string) ([]datasource.Metric, error) {
return []datasource.Metric{
{
Labels: []prompbmarshal.Label{
{Name: "foo", Value: "bar"},
{Name: "baz", Value: "qux"},
},
Values: []float64{1},
Timestamps: []int64{1},
},
{
Labels: []prompbmarshal.Label{
{Name: "foo", Value: "garply"},
{Name: "baz", Value: "fred"},
},
Values: []float64{2},
Timestamps: []int64{1},
},
}, nil
}
tpl, err := alert.ExecTemplate(qFn, alert.Labels, annotations)
if err != nil {
t.Fatalf("cannot execute template: %s", err)
}
if len(tpl) != len(tplExpected) {
t.Fatalf("unexpected number of elements; got %d; want %d", len(tpl), len(tplExpected))
}
for k := range tplExpected {
got, exp := tpl[k], tplExpected[k]
if got != exp {
t.Fatalf("unexpected template for key=%q; got %q; want %q", k, got, exp)
}
}
}
// empty-alert
f(&Alert{}, map[string]string{}, map[string]string{})
// no-template
f(&Alert{
Value: 1e4,
Labels: map[string]string{
"instance": "localhost",
},
}, map[string]string{
"summary": "it's a test summary",
"description": "it's a test description",
}, map[string]string{
"summary": "it's a test summary",
"description": "it's a test description",
})
// label-template
f(&Alert{
Value: 1e4,
Labels: map[string]string{
"job": "staging",
"instance": "localhost",
},
For: 5 * time.Minute,
}, map[string]string{
"summary": "Too high connection number for {{$labels.instance}} for job {{$labels.job}}",
"description": "It is {{ $value }} connections for {{$labels.instance}} for more than {{ .For }}",
}, map[string]string{
"summary": "Too high connection number for localhost for job staging",
"description": "It is 10000 connections for localhost for more than 5m0s",
})
// label template override
f(&Alert{
Value: 1e4,
}, map[string]string{
"summary": `{{- define "default.template" -}} {{ printf "summary" }} {{- end -}} {{ template "default.template" . }}`,
"description": `{{ template "default.template" . }}`,
"value": `{{$value }}`,
}, map[string]string{
"summary": "summary",
"description": "",
"value": "10000",
})
// expression-template
f(&Alert{
Expr: `vm_rows{"label"="bar"}<0`,
}, map[string]string{
"exprEscapedQuery": "{{ $expr|queryEscape }}",
"exprEscapedPath": "{{ $expr|pathEscape }}",
"exprEscapedJSON": "{{ $expr|jsonEscape }}",
"exprEscapedQuotes": "{{ $expr|quotesEscape }}",
"exprEscapedHTML": "{{ $expr|htmlEscape }}",
}, map[string]string{
"exprEscapedQuery": "vm_rows%7B%22label%22%3D%22bar%22%7D%3C0",
"exprEscapedPath": "vm_rows%7B%22label%22=%22bar%22%7D%3C0",
"exprEscapedJSON": `"vm_rows{\"label\"=\"bar\"}\u003c0"`,
"exprEscapedQuotes": `vm_rows{\"label\"=\"bar\"}\u003c0`,
"exprEscapedHTML": "vm_rows{&quot;label&quot;=&quot;bar&quot;}&lt;0",
})
// query
f(&Alert{
Expr: `vm_rows{"label"="bar"}>0`,
}, map[string]string{
"summary": `{{ query "foo" | first | value }}`,
"desc": `{{ range query "bar" }}{{ . | label "foo" }} {{ . | value }};{{ end }}`,
}, map[string]string{
"summary": "1",
"desc": "bar 1;garply 2;",
})
// external
f(&Alert{
Value: 1e4,
Labels: map[string]string{
"job": "staging",
"instance": "localhost",
},
}, map[string]string{
"url": "{{ $externalURL }}",
"summary": "Issues with {{$labels.instance}} (dc-{{$externalLabels.dc}}) for job {{$labels.job}}",
"description": "It is {{ $value }} connections for {{$labels.instance}} (cluster-{{$externalLabels.cluster}})",
}, map[string]string{
"url": extURL,
"summary": fmt.Sprintf("Issues with localhost (dc-%s) for job staging", extDC),
"description": fmt.Sprintf("It is 10000 connections for localhost (cluster-%s)", extCluster),
})
// alert and group IDs
f(&Alert{
ID: 42,
GroupID: 24,
}, map[string]string{
"url": "/api/v1/alert?alertID={{$alertID}}&groupID={{$groupID}}",
}, map[string]string{
"url": "/api/v1/alert?alertID=42&groupID=24",
})
// ActiveAt time
f(&Alert{
ActiveAt: time.Date(2022, 8, 19, 20, 34, 58, 651387237, time.UTC),
}, map[string]string{
"diagram": "![](http://example.com?render={{$activeAt.Unix}}",
}, map[string]string{
"diagram": "![](http://example.com?render=1660941298",
})
// ActiveAt time is nil
f(&Alert{}, map[string]string{
"default_time": "{{$activeAt}}",
}, map[string]string{
"default_time": "0001-01-01 00:00:00 +0000 UTC",
})
// ActiveAt custom format
f(&Alert{
ActiveAt: time.Date(2022, 8, 19, 20, 34, 58, 651387237, time.UTC),
}, map[string]string{
"fire_time": `{{$activeAt.Format "2006/01/02 15:04:05"}}`,
}, map[string]string{
"fire_time": "2022/08/19 20:34:58",
})
// ActiveAt query range
f(&Alert{
ActiveAt: time.Date(2022, 8, 19, 20, 34, 58, 651387237, time.UTC),
}, map[string]string{
"grafana_url": `vm-grafana.com?from={{($activeAt.Add (parseDurationTime "1h")).Unix}}&to={{($activeAt.Add (parseDurationTime "-1h")).Unix}}`,
}, map[string]string{
"grafana_url": "vm-grafana.com?from=1660944898&to=1660937698",
})
}
func TestAlert_toPromLabels(t *testing.T) {
fn := func(labels map[string]string, exp []prompbmarshal.Label, relabel *promrelabel.ParsedConfigs) {
t.Helper()

View File

@@ -3,7 +3,6 @@ package notifier
import (
"flag"
"fmt"
"net/url"
"strings"
"time"
@@ -73,15 +72,6 @@ func Reload() error {
var staticNotifiersFn func() []Notifier
var (
// externalLabels is a global variable for holding external labels configured via flags
// It is supposed to be inited via Init function only.
externalLabels map[string]string
// externalURL is a global variable for holding external URL value configured via flag
// It is supposed to be inited via Init function only.
externalURL string
)
// Init returns a function for retrieving actual list of Notifier objects.
// Init works in two mods:
// - configuration via flags (for backward compatibility). Is always static
@@ -89,14 +79,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) {
externalURL = extURL
externalLabels = extLabels
_, err := url.Parse(externalURL)
if err != nil {
return nil, fmt.Errorf("failed to parse external URL: %w", err)
}
func Init(gen AlertURLGenerator) (func() []Notifier, error) {
if *blackHole {
if len(*addrs) > 0 || *configPath != "" {
return nil, fmt.Errorf("only one of -notifier.blackhole, -notifier.url and -notifier.config flags must be specified")
@@ -126,6 +109,7 @@ func Init(gen AlertURLGenerator, extLabels map[string]string, extURL string) (fu
return staticNotifiersFn, nil
}
var err error
cw, err = newWatcher(*configPath, gen)
if err != nil {
return nil, fmt.Errorf("failed to init config watcher: %w", err)

View File

@@ -12,7 +12,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 +52,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 +69,7 @@ func TestBlackHole(t *testing.T) {
*blackHole = true
fn, err := Init(nil, nil, "")
fn, err := Init(nil)
if err != nil {
t.Fatalf("%s", err)
}

View File

@@ -9,7 +9,7 @@ import (
)
func TestMain(m *testing.M) {
if err := templates.Load([]string{"testdata/templates/*good.tmpl"}, url.URL{}); err != nil {
if err := templates.Init([]string{"testdata/templates/*good.tmpl"}, nil, url.URL{}); err != nil {
os.Exit(1)
}
os.Exit(m.Run())

View File

@@ -7,6 +7,7 @@ import (
"sort"
"strings"
"sync"
textTpl "text/template"
"time"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/config"
@@ -47,6 +48,12 @@ type AlertingRule struct {
state *ruleState
metrics *alertingRuleMetrics
// set rootTemplateName with loaded global template name,
// so we can check if the template has changed when evaluating.
rootTemplateName string
LabelTemplates map[string]*textTpl.Template
AnnotationsTemplates map[string]*textTpl.Template
}
type alertingRuleMetrics struct {
@@ -142,9 +149,47 @@ func NewAlertingRule(qb datasource.QuerierBuilder, group *Group, cfg config.Rule
}
return seriesFetched
})
ar.initTemplate()
return ar
}
// initTemplate pre-creates templates that can be reused in execution.
func (ar *AlertingRule) initTemplate() {
currentTmpl := templates.GetCurrentTmpl()
ar.rootTemplateName = currentTmpl.Name()
ar.LabelTemplates = make(map[string]*textTpl.Template, len(ar.Labels))
ar.AnnotationsTemplates = make(map[string]*textTpl.Template, len(ar.Annotations))
for k, v := range ar.Labels {
var err error
tmpl, _ := currentTmpl.Clone()
tmpl, err = templates.ParseWithFixedHeader(v, tmpl)
if err != nil {
// parse can fail in two cases:
// 1. the text contains `query` function, which is not supported during rule initialization.
// 2. the text itself is invalid.
// In both case, we skip the error here, and try it again during rule execution.
continue
}
ar.LabelTemplates[k] = tmpl
}
for k, v := range ar.Annotations {
var err error
tmpl, _ := currentTmpl.Clone()
tmpl, err = templates.ParseWithFixedHeader(v, tmpl)
if err != nil {
// parse can fail in two cases:
// 1. the text contains `query` function, which is not supported during rule initialization.
// 2. the text itself is invalid.
// In both case, we skip the error here, and try it again during rule execution.
continue
}
ar.AnnotationsTemplates[k] = tmpl
}
}
// close unregisters rule metrics
func (ar *AlertingRule) close() {
ar.metrics.active.Unregister()
@@ -225,6 +270,8 @@ func (ar *AlertingRule) updateWith(r Rule) error {
ar.KeepFiringFor = nr.KeepFiringFor
ar.Labels = nr.Labels
ar.Annotations = nr.Annotations
ar.initTemplate()
ar.EvalInterval = nr.EvalInterval
ar.Debug = nr.Debug
ar.q = nr.q
@@ -279,15 +326,32 @@ func (ar *AlertingRule) toLabels(m datasource.Metric, qFn templates.QueryFn) (*l
}
ls.processed[l.Name] = l.Value
}
extraLabels, err := notifier.ExecTemplate(qFn, ar.Labels, notifier.AlertTplData{
extraLabels := make(map[string]string, len(ar.Labels))
// compare to annotation, label value can only use limited variables for now
labelTplData := templates.AlertTplData{
Labels: ls.origin,
Value: m.Values[0],
Expr: ar.Expr,
})
if err != nil {
return nil, fmt.Errorf("failed to expand labels: %w", err)
}
for k := range ar.Labels {
if ar.LabelTemplates[k] == nil {
// this label may contain `query` function, which requires creating new template with query function in each evaluation.
v, err := templates.ExecuteWithoutTemplate(qFn, ar.Labels[k], labelTplData)
if err != nil {
logger.Errorf("error templating label %q for rule %q: %w", ar.Labels[k], ar.Name, err)
v = ar.Labels[k]
}
extraLabels[k] = v
continue
}
v, err := templates.ExecuteWithTemplate(labelTplData, ar.LabelTemplates[k])
if err != nil {
logger.Errorf("error templating label %q for rule %q: %w", ar.Labels[k], ar.Name, err)
v = ar.Labels[k]
}
extraLabels[k] = v
}
for k, v := range extraLabels {
ls.add(k, v)
}
@@ -513,12 +577,18 @@ func (ar *AlertingRule) exec(ctx context.Context, ts time.Time, limit int) ([]pr
}
func (ar *AlertingRule) expandTemplates(m datasource.Metric, qFn templates.QueryFn, ts time.Time) (*labelSet, map[string]string, error) {
// check if the rule template has changed during reload,
// if so, label&annotation templates must be re-created.
if ar.rootTemplateName != templates.GetCurrentTmpl().Name() {
ar.initTemplate()
}
ls, err := ar.toLabels(m, qFn)
if err != nil {
return nil, nil, fmt.Errorf("failed to expand labels: %w", err)
}
tplData := notifier.AlertTplData{
extraAnnotation := make(map[string]string, len(ar.Annotations))
annotationTplData := templates.AlertTplData{
Value: m.Values[0],
Labels: ls.origin,
Expr: ar.Expr,
@@ -527,11 +597,26 @@ func (ar *AlertingRule) expandTemplates(m datasource.Metric, qFn templates.Query
ActiveAt: ts,
For: ar.For,
}
as, err := notifier.ExecTemplate(qFn, ar.Annotations, tplData)
if err != nil {
return nil, nil, fmt.Errorf("failed to template annotations: %w", err)
for k := range ar.Annotations {
if ar.AnnotationsTemplates[k] == nil {
// this label may contain `query` function, which requires creating new template with query function in each evaluation.
v, err := templates.ExecuteWithoutTemplate(qFn, ar.Annotations[k], annotationTplData)
if err != nil {
logger.Errorf("error templating annotation %q for rule %q: %w", ar.Annotations[k], ar.Name, err)
v = ar.Annotations[k]
}
extraAnnotation[k] = v
continue
}
v, err := templates.ExecuteWithTemplate(annotationTplData, ar.AnnotationsTemplates[k])
if err != nil {
logger.Errorf("error templating annotation %q for rule %q: %w", ar.Annotations[k], ar.Name, err)
v = ar.Annotations[k]
}
extraAnnotation[k] = v
}
return ls, as, nil
return ls, extraAnnotation, nil
}
// toTimeSeries creates `ALERTS` and `ALERTS_FOR_STATE` for active alerts

View File

@@ -1046,6 +1046,7 @@ func TestAlertingRule_Template(t *testing.T) {
}
fq.Add(metrics...)
rule.initTemplate()
if _, err := rule.exec(context.TODO(), time.Now(), 0); err != nil {
t.Fatalf("unexpected error: %s", err)
}
@@ -1262,6 +1263,7 @@ func newTestAlertingRuleWithCustomFields(name string, waitFor, evalInterval, kee
}
rule.KeepFiringFor = keepFiringFor
rule.Annotations = annotation
rule.initTemplate()
return rule
}

View File

@@ -443,8 +443,8 @@ func (g *Group) Start(ctx context.Context, nts func() []notifier.Notifier, rw re
}
// UpdateWith inserts new group to updateCh
func (g *Group) UpdateWith(newGroup *Group) {
g.updateCh <- newGroup
func (g *Group) UpdateWith(new *Group) {
g.updateCh <- new
}
// DeepCopy returns a deep copy of group

View File

@@ -27,7 +27,7 @@ func init() {
}
func TestMain(m *testing.M) {
if err := templates.Load([]string{}, url.URL{}); err != nil {
if err := templates.Init([]string{}, nil, url.URL{}); err != nil {
fmt.Println("failed to load template for test")
os.Exit(1)
}

View File

@@ -0,0 +1,553 @@
package templates
import (
"errors"
"fmt"
htmlTpl "html/template"
"io"
"math"
"net"
"net/url"
"regexp"
"sort"
"strconv"
"strings"
"sync"
textTpl "text/template"
"time"
"github.com/bmatcuk/doublestar/v4"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/datasource"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/formatutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promutils"
)
var (
tplMu sync.RWMutex
masterTmpl *textTpl.Template
// externalLabels is a global variable for holding external labels configured via flags
// It is supposed to be initiated via Init function only.
externalLabels map[string]string
// externalURL is a global variable for holding external URL value configured via flag
// It is supposed to be initiated via Init function only.
externalURL url.URL
)
// Init initializes global externalLabels and externalURL variables, and load templates from pathPatterns.
func Init(pathPatterns []string, extLabels map[string]string, extURL url.URL) error {
externalURL = extURL
externalLabels = extLabels
return LoadTemplateFile(pathPatterns)
}
// LoadTemplateFile loads templates from multiple globs specified in pathPatterns:
// 1. if it's the first load, sets them directly to current template;
// 2. if it's not the first load, only update masterTmpl when the contents change.
func LoadTemplateFile(pathPatterns []string) error {
templateName := fmt.Sprintf("rule-template-%d", time.Now().UnixMilli())
// using Load timestamp as template root name,
// so we can check if this global template has been reloaded when use it elsewhere, like during alerting rules execution.
tmpl := newTemplate(templateName)
tmpl = tmpl.Funcs(funcsWithExternalURL(externalURL))
for _, tp := range pathPatterns {
p, err := doublestar.FilepathGlob(tp)
if err != nil {
return fmt.Errorf("failed to retrieve a template glob %q: %w", tp, err)
}
if len(p) > 0 {
tmpl, err = tmpl.ParseFiles(p...)
if err != nil {
return fmt.Errorf("failed to parse template glob %q: %w", tp, err)
}
}
}
err := tmpl.Execute(io.Discard, nil)
if err != nil {
return fmt.Errorf("failed to test rule template: %w", err)
}
tplMu.Lock()
defer tplMu.Unlock()
if masterTmpl == nil {
masterTmpl = tmpl
} else {
// only update the masterTmpl when content has changed
if !isTemplatesTheSame(masterTmpl, tmpl) {
masterTmpl = tmpl
}
}
return nil
}
func newTemplate(name string) *textTpl.Template {
tmpl := textTpl.New(name).Funcs(templateFuncs())
return textTpl.Must(tmpl.Parse(""))
}
// isTemplatesTheSame returns true if the content of two templates are the same,
// the root template name difference is ignored.
func isTemplatesTheSame(t1, t2 *textTpl.Template) bool {
getRootString := func(t *textTpl.Template) string {
if t.Tree == nil || t.Tree.Root == nil {
return ""
}
return t.Tree.Root.String()
}
if getRootString(t1) != getRootString(t2) {
return false
}
t1Templates := make(map[string]string)
t2Templates := make(map[string]string)
for _, tmpl := range t1.Templates() {
// skip root template, since it's null and changes every time
if tmpl.Name() == t1.Name() {
continue
}
t1Templates[tmpl.Name()] = getRootString(tmpl)
}
for _, tmpl := range t2.Templates() {
if tmpl.Name() == t2.Name() {
continue
}
t2Templates[tmpl.Name()] = getRootString(tmpl)
}
if len(t1Templates) != len(t2Templates) {
return false
}
for k, v := range t1Templates {
if t2Templates[k] != v {
return false
}
delete(t2Templates, k)
}
return len(t2Templates) == 0
}
// funcsWithExternalURL returns a function map that depends on externalURL value
func funcsWithExternalURL(externalURL url.URL) textTpl.FuncMap {
return textTpl.FuncMap{
"externalURL": func() string {
return externalURL.String()
},
"pathPrefix": func() string {
return externalURL.Path
},
}
}
// templateFuncs initiates template helper functions
func templateFuncs() textTpl.FuncMap {
// See https://prometheus.io/docs/prometheus/latest/configuration/template_reference/
// and https://github.com/prometheus/prometheus/blob/fa6e05903fd3ce52e374a6e1bf4eb98c9f1f45a7/template/template.go#L150
return textTpl.FuncMap{
/* Strings */
// title returns a copy of the string s with all Unicode letters
// that begin words mapped to their Unicode title case.
// alias for https://golang.org/pkg/strings/#Title
"title": strings.Title,
// toUpper returns s with all Unicode letters mapped to their upper case.
// alias for https://golang.org/pkg/strings/#ToUpper
"toUpper": strings.ToUpper,
// toLower returns s with all Unicode letters mapped to their lower case.
// alias for https://golang.org/pkg/strings/#ToLower
"toLower": strings.ToLower,
// crlfEscape replaces '\n' and '\r' chars with `\\n` and `\\r`.
// This function is deprecated.
//
// It is better to use quotesEscape, jsonEscape, queryEscape or pathEscape instead -
// these functions properly escape `\n` and `\r` chars according to their purpose.
"crlfEscape": func(q string) string {
q = strings.Replace(q, "\n", `\n`, -1)
return strings.Replace(q, "\r", `\r`, -1)
},
// quotesEscape escapes the string, so it can be safely put inside JSON string.
//
// See also jsonEscape.
"quotesEscape": quotesEscape,
// jsonEscape converts the string to properly encoded JSON string.
//
// See also quotesEscape.
"jsonEscape": jsonEscape,
// htmlEscape applies html-escaping to q, so it can be safely embedded as plaintext into html.
//
// See also safeHtml.
"htmlEscape": htmlEscape,
// stripPort splits string into host and port, then returns only host.
"stripPort": func(hostPort string) string {
host, _, err := net.SplitHostPort(hostPort)
if err != nil {
return hostPort
}
return host
},
// stripDomain removes the domain part of a FQDN. Leaves port untouched.
"stripDomain": func(hostPort string) string {
host, port, err := net.SplitHostPort(hostPort)
if err != nil {
host = hostPort
}
ip := net.ParseIP(host)
if ip != nil {
return hostPort
}
host = strings.Split(host, ".")[0]
if port != "" {
return net.JoinHostPort(host, port)
}
return host
},
// match reports whether the string s
// contains any match of the regular expression pattern.
// alias for https://golang.org/pkg/regexp/#MatchString
"match": regexp.MatchString,
// reReplaceAll ReplaceAllString returns a copy of src, replacing matches of the Regexp with
// the replacement string repl. Inside repl, $ signs are interpreted as in Expand,
// so for instance $1 represents the text of the first submatch.
// alias for https://golang.org/pkg/regexp/#Regexp.ReplaceAllString
"reReplaceAll": func(pattern, repl, text string) string {
re := regexp.MustCompile(pattern)
return re.ReplaceAllString(text, repl)
},
// parseDuration parses a duration string such as "1h" into the number of seconds it represents
"parseDuration": func(s string) (float64, error) {
d, err := promutils.ParseDuration(s)
if err != nil {
return 0, err
}
return d.Seconds(), nil
},
// same with parseDuration but returns a time.Duration
"parseDurationTime": func(s string) (time.Duration, error) {
d, err := promutils.ParseDuration(s)
if err != nil {
return 0, err
}
return d, nil
},
/* Numbers */
// humanize converts given number to a human readable format
// by adding metric prefixes https://en.wikipedia.org/wiki/Metric_prefix
"humanize": func(i any) (string, error) {
v, err := toFloat64(i)
if err != nil {
return "", err
}
if v == 0 || math.IsNaN(v) || math.IsInf(v, 0) {
return fmt.Sprintf("%.4g", v), nil
}
if math.Abs(v) >= 1 {
prefix := ""
for _, p := range []string{"k", "M", "G", "T", "P", "E", "Z", "Y"} {
if math.Abs(v) < 1000 {
break
}
prefix = p
v /= 1000
}
return fmt.Sprintf("%.4g%s", v, prefix), nil
}
prefix := ""
for _, p := range []string{"m", "u", "n", "p", "f", "a", "z", "y"} {
if math.Abs(v) >= 1 {
break
}
prefix = p
v *= 1000
}
return fmt.Sprintf("%.4g%s", v, prefix), nil
},
// humanize1024 converts given number to a human readable format with 1024 as base
"humanize1024": func(i any) (string, error) {
v, err := toFloat64(i)
if err != nil {
return "", err
}
if math.Abs(v) <= 1 || math.IsNaN(v) || math.IsInf(v, 0) {
return fmt.Sprintf("%.4g", v), nil
}
return formatutil.HumanizeBytes(v), nil
},
// humanizeDuration converts given seconds to a human-readable duration
"humanizeDuration": func(i any) (string, error) {
v, err := toFloat64(i)
if err != nil {
return "", err
}
if math.IsNaN(v) || math.IsInf(v, 0) {
return fmt.Sprintf("%.4g", v), nil
}
if v == 0 {
return fmt.Sprintf("%.4gs", v), nil
}
if math.Abs(v) >= 1 {
sign := ""
if v < 0 {
sign = "-"
v = -v
}
seconds := int64(v) % 60
minutes := (int64(v) / 60) % 60
hours := (int64(v) / 60 / 60) % 24
days := int64(v) / 60 / 60 / 24
// For days to minutes, we display seconds as an integer.
if days != 0 {
return fmt.Sprintf("%s%dd %dh %dm %ds", sign, days, hours, minutes, seconds), nil
}
if hours != 0 {
return fmt.Sprintf("%s%dh %dm %ds", sign, hours, minutes, seconds), nil
}
if minutes != 0 {
return fmt.Sprintf("%s%dm %ds", sign, minutes, seconds), nil
}
// For seconds, we display 4 significant digits.
return fmt.Sprintf("%s%.4gs", sign, v), nil
}
prefix := ""
for _, p := range []string{"m", "u", "n", "p", "f", "a", "z", "y"} {
if math.Abs(v) >= 1 {
break
}
prefix = p
v *= 1000
}
return fmt.Sprintf("%.4g%ss", v, prefix), nil
},
// humanizePercentage converts given ratio value to a fraction of 100
"humanizePercentage": func(i any) (string, error) {
v, err := toFloat64(i)
if err != nil {
return "", err
}
return fmt.Sprintf("%.4g%%", v*100), nil
},
// humanizeTimestamp converts given timestamp to a human readable time equivalent
"humanizeTimestamp": func(i any) (string, error) {
v, err := toFloat64(i)
if err != nil {
return "", err
}
if math.IsNaN(v) || math.IsInf(v, 0) {
return fmt.Sprintf("%.4g", v), nil
}
t := timeFromUnixTimestamp(v).Time().UTC()
return fmt.Sprint(t), nil
},
// toTime converts given timestamp to a time.Time.
"toTime": func(i any) (time.Time, error) {
v, err := toFloat64(i)
if err != nil {
return time.Time{}, err
}
if math.IsNaN(v) || math.IsInf(v, 0) {
return time.Time{}, fmt.Errorf("cannot convert %v to time.Time", v)
}
t := timeFromUnixTimestamp(v).Time().UTC()
return t, nil
},
/* URLs */
// externalURL returns value of `external.url` flag
"externalURL": func() string {
// externalURL function supposed to be substituted at FuncsWithExteralURL().
// it is present here only for validation purposes, when there is no
// provided datasource.
//
// return non-empty slice to pass validation with chained functions in template
return ""
},
// pathPrefix returns a Path segment from the URL value in `external.url` flag
"pathPrefix": func() string {
// pathPrefix function supposed to be substituted at FuncsWithExteralURL().
// it is present here only for validation purposes, when there is no
// provided datasource.
//
// return non-empty slice to pass validation with chained functions in template
return ""
},
// pathEscape escapes the string so it can be safely placed inside a URL path segment.
//
// See also queryEscape.
"pathEscape": url.PathEscape,
// queryEscape escapes the string so it can be safely placed inside a query arg in URL.
//
// See also queryEscape.
"queryEscape": url.QueryEscape,
// first returns the first by order element from the given metrics list.
// usually used alongside with `query` template function.
"first": func(metrics []metric) (metric, error) {
if len(metrics) > 0 {
return metrics[0], nil
}
return metric{}, errors.New("first() called on vector with no elements")
},
// label returns the value of the given label name for the given metric.
// usually used alongside with `query` template function.
"label": func(label string, m metric) string {
return m.Labels[label]
},
// value returns the value of the given metric.
// usually used alongside with `query` template function.
"value": func(m metric) float64 {
return m.Value
},
// strvalue returns metric name.
"strvalue": func(m metric) string {
return m.Labels["__name__"]
},
// sortByLabel sorts the given metrics by provided label key
"sortByLabel": func(label string, metrics []metric) []metric {
sort.SliceStable(metrics, func(i, j int) bool {
return metrics[i].Labels[label] < metrics[j].Labels[label]
})
return metrics
},
/* Helpers */
// Converts a list of objects to a map with keys arg0, arg1 etc.
// This is intended to allow multiple arguments to be passed to templates.
"args": func(args ...any) map[string]any {
result := make(map[string]any)
for i, a := range args {
result[fmt.Sprintf("arg%d", i)] = a
}
return result
},
// safeHtml marks string as HTML not requiring auto-escaping.
//
// See also htmlEscape.
"safeHtml": func(text string) htmlTpl.HTML {
return htmlTpl.HTML(text)
},
}
}
// metric is private copy of datasource.Metric,
// it is used for templating annotations,
// Labels as map simplifies templates evaluation.
type metric struct {
Labels map[string]string
Timestamp int64
Value float64
}
// datasourceMetricsToTemplateMetrics converts Metrics from datasource package to private copy for templating.
func datasourceMetricsToTemplateMetrics(ms []datasource.Metric) []metric {
mss := make([]metric, 0, len(ms))
for _, m := range ms {
labelsMap := make(map[string]string, len(m.Labels))
for _, labelValue := range m.Labels {
labelsMap[labelValue.Name] = labelValue.Value
}
mss = append(mss, metric{
Labels: labelsMap,
Timestamp: m.Timestamps[0],
Value: m.Values[0]})
}
return mss
}
// QueryFn is used to wrap a call to datasource into simple-to-use function
// for templating functions.
type QueryFn func(query string) ([]datasource.Metric, error)
// FuncsWithQuery returns a function map that depends on metric data
func FuncsWithQuery(query QueryFn) textTpl.FuncMap {
return textTpl.FuncMap{
"query": func(q string) ([]metric, error) {
if query == nil {
return nil, fmt.Errorf("cannot execute query %q: query is not available in this context", q)
}
result, err := query(q)
if err != nil {
return nil, err
}
return datasourceMetricsToTemplateMetrics(result), nil
},
}
}
// Time is the number of milliseconds since the epoch
// (1970-01-01 00:00 UTC) excluding leap seconds.
type Time int64
// timeFromUnixTimestamp returns the Time equivalent to t in unix timestamp.
func timeFromUnixTimestamp(t float64) Time {
return Time(t * 1e3)
}
// The number of nanoseconds per minimum tick.
const nanosPerTick = int64(minimumTick / time.Nanosecond)
// MinimumTick is the minimum supported time resolution. This has to be
// at least time.Second in order for the code below to work.
const minimumTick = time.Millisecond
// second is the Time duration equivalent to one second.
const second = int64(time.Second / minimumTick)
// Time returns the time.Time representation of t.
func (t Time) Time() time.Time {
return time.Unix(int64(t)/second, (int64(t)%second)*nanosPerTick)
}
func toFloat64(v any) (float64, error) {
switch i := v.(type) {
case float64:
return i, nil
case float32:
return float64(i), nil
case int64:
return float64(i), nil
case int32:
return float64(i), nil
case int:
return float64(i), nil
case uint64:
return float64(i), nil
case uint32:
return float64(i), nil
case uint:
return float64(i), nil
case string:
return strconv.ParseFloat(i, 64)
default:
return 0, fmt.Errorf("unexpected value type %v", i)
}
}

View File

@@ -0,0 +1,261 @@
package templates
import (
"fmt"
"math"
"net/url"
"os"
"strings"
"testing"
textTpl "text/template"
)
func TestMain(m *testing.M) {
if err := Init([]string{}, nil, url.URL{}); err != nil {
fmt.Println("failed to load template for test")
os.Exit(1)
}
os.Exit(m.Run())
}
func TestTemplateFuncs_Match(t *testing.T) {
funcs := templateFuncs()
// check "match" func
matchFunc := funcs["match"].(func(pattern, s string) (bool, error))
if _, err := matchFunc("invalid[regexp", "abc"); err == nil {
t.Fatalf("expecting non-nil error on invalid regexp")
}
ok, err := matchFunc("abc", "def")
if err != nil {
t.Fatalf("unexpected error")
}
if ok {
t.Fatalf("unexpected match")
}
ok, err = matchFunc("a.+b", "acsdb")
if err != nil {
t.Fatalf("unexpected error")
}
if !ok {
t.Fatalf("unexpected mismatch")
}
}
func TestTemplateFuncs_Formatting(t *testing.T) {
f := func(funcName string, p any, resultExpected string) {
t.Helper()
funcs := templateFuncs()
v := funcs[funcName]
fLocal := v.(func(s any) (string, error))
result, err := fLocal(p)
if err != nil {
t.Fatalf("unexpected error for %s(%f): %s", funcName, p, err)
}
if result != resultExpected {
t.Fatalf("unexpected result for %s(%f); got\n%s\nwant\n%s", funcName, p, result, resultExpected)
}
}
f("humanize1024", float64(0), "0")
f("humanize1024", math.Inf(0), "+Inf")
f("humanize1024", math.NaN(), "NaN")
f("humanize1024", float64(127087), "124.1ki")
f("humanize1024", float64(130137088), "124.1Mi")
f("humanize1024", float64(133260378112), "124.1Gi")
f("humanize1024", float64(136458627186688), "124.1Ti")
f("humanize1024", float64(139733634239168512), "124.1Pi")
f("humanize1024", float64(143087241460908556288), "124.1Ei")
f("humanize1024", float64(146521335255970361638912), "124.1Zi")
f("humanize1024", float64(150037847302113650318245888), "124.1Yi")
f("humanize1024", float64(153638755637364377925883789312), "1.271e+05Yi")
f("humanize", float64(127087), "127.1k")
f("humanize", float64(136458627186688), "136.5T")
f("humanizeDuration", 1, "1s")
f("humanizeDuration", 0.2, "200ms")
f("humanizeDuration", 42000, "11h 40m 0s")
f("humanizeDuration", 16790555, "194d 8h 2m 35s")
f("humanizePercentage", 1, "100%")
f("humanizePercentage", 0.8, "80%")
f("humanizePercentage", 0.015, "1.5%")
f("humanizeTimestamp", 1679055557, "2023-03-17 12:19:17 +0000 UTC")
}
func TestTemplateFuncs_StringConversion(t *testing.T) {
f := func(funcName, s, resultExpected string) {
t.Helper()
funcs := templateFuncs()
v := funcs[funcName]
fLocal := v.(func(s string) string)
result := fLocal(s)
if result != resultExpected {
t.Fatalf("unexpected result for %s(%q); got\n%s\nwant\n%s", funcName, s, result, resultExpected)
}
}
f("title", "foo bar", "Foo Bar")
f("toUpper", "foo", "FOO")
f("toLower", "FOO", "foo")
f("pathEscape", "foo/bar\n+baz", "foo%2Fbar%0A+baz")
f("queryEscape", "foo+bar\n+baz", "foo%2Bbar%0A%2Bbaz")
f("jsonEscape", `foo{bar="baz"}`+"\n + 1", `"foo{bar=\"baz\"}\n + 1"`)
f("quotesEscape", `foo{bar="baz"}`+"\n + 1", `foo{bar=\"baz\"}\n + 1`)
f("htmlEscape", "foo < 10\nabc", "foo &lt; 10\nabc")
f("crlfEscape", "foo\nbar\rx", `foo\nbar\rx`)
f("stripPort", "foo", "foo")
f("stripPort", "foo:1234", "foo")
f("stripDomain", "foo.bar.baz", "foo")
f("stripDomain", "foo.bar:123", "foo:123")
}
func TestTemplatesLoad_Success(t *testing.T) {
f := func(pathPatterns []string, expectedTmpl *textTpl.Template) {
t.Helper()
masterTmplOrig := masterTmpl
defer func() {
masterTmpl = masterTmplOrig
}()
masterTmpl = nil
if err := LoadTemplateFile(pathPatterns); err != nil {
t.Fatalf("cannot load templates: %s", err)
}
if !isTemplatesTheSame(masterTmpl, expectedTmpl) {
t.Fatalf("unexpected template\ngot\n%+v\nwant\n%+v", masterTmpl, expectedTmpl)
}
}
// non existing path
pathPatterns := []string{
"templates/non-existing/good-*.tpl",
"templates/absent/good-*.tpl",
}
expectedTmpl := textTpl.Must(newTemplate("").Parse(""))
f(pathPatterns, expectedTmpl)
// existing path
pathPatterns = []string{
"templates/test/good0-*.tpl",
}
expectedTmpl = textTpl.Must(newTemplate("").Parse(`
{{- define "good0-test.tpl" -}}{{- end -}}
{{- define "test.0" -}}
{{ printf "Hello %s!" externalURL }}
{{- end -}}
{{- define "test.2" -}}
{{ printf "Hello %s!" externalURL }}
{{- end -}}
{{- define "test.3" -}}
{{ printf "Hello %s!" externalURL }}
{{- end -}}
`))
f(pathPatterns, expectedTmpl)
// template update
pathPatterns = []string{
"templates/other/nested/good0-*.tpl",
}
expectedTmpl = textTpl.Must(newTemplate("").Parse(`
{{- define "good0-test.tpl" -}}{{- end -}}
{{- define "test.0" -}}
{{ printf "Hello %s!" externalURL }}
{{- end -}}
{{- define "test.1" -}}
{{ printf "Hello %s!" externalURL }}
{{- end -}}
{{- define "test.3" -}}
{{ printf "Hello %s!" externalURL }}
{{- end -}}
`))
f(pathPatterns, expectedTmpl)
}
func TestTemplatesLoad_Failure(t *testing.T) {
f := func(pathPatterns []string, expectedErrStr string) {
t.Helper()
err := LoadTemplateFile(pathPatterns)
if err == nil {
t.Fatalf("expecting non-nil error")
}
errStr := err.Error()
if !strings.Contains(errStr, expectedErrStr) {
t.Fatalf("the returned error %q doesn't contain %q", errStr, expectedErrStr)
}
}
// load template with syntax error
f([]string{
"templates/other/nested/bad0-*.tpl",
"templates/test/good0-*.tpl",
}, "failed to parse template glob")
}
func TestTemplatesReload(t *testing.T) {
masterTmplOrig := masterTmpl
defer func() {
masterTmpl = masterTmplOrig
}()
masterTmpl = nil
// load with non existing path
pathPatterns := []string{
"templates/non-existing/good-*.tpl",
"templates/absent/good-*.tpl",
}
if err := LoadTemplateFile(pathPatterns); err != nil {
t.Fatalf("cannot load templates: %s", err)
}
tpl1 := GetCurrentTmpl()
// reload with existing path
pathPatterns = []string{
"templates/test/good0-*.tpl",
}
if err := LoadTemplateFile(pathPatterns); err != nil {
t.Fatalf("cannot load templates: %s", err)
}
tpl2 := GetCurrentTmpl()
if isTemplatesTheSame(tpl1, tpl2) {
t.Fatalf("tpl1 should be different from tpl2")
}
// reload the same path
pathPatterns = []string{
"templates/test/good0-*.tpl",
}
if err := LoadTemplateFile(pathPatterns); err != nil {
t.Fatalf("cannot load templates: %s", err)
}
tpl3 := GetCurrentTmpl()
if !isTemplatesTheSame(tpl2, tpl3) || tpl2.Name() != tpl3.Name() {
t.Fatalf("tpl3 should be the same as tpl2")
}
}
func TestIsTemplateTheSame(t *testing.T) {
f := func(tmpl1, tmpl2 *textTpl.Template, isTheSame bool) {
t.Helper()
if isTemplatesTheSame(tmpl1, tmpl2) != isTheSame {
t.Fatalf("unexpected result for isTemplatesTheSame")
}
}
tmpl1 := textTpl.Must(newTemplate("t1").Parse("{{- define \"test\" -}}{{- end -}}"))
tmpl2 := textTpl.Must(newTemplate("t2").Parse("{{- define \"test\" -}}{{- end -}}"))
f(tmpl1, tmpl2, true)
tmpl1, _ = tmpl1.Parse("{{- define \"test2\" -}}{{- end -}}")
f(tmpl1, tmpl2, false)
tmpl2, _ = tmpl2.Parse("{{- define \"test3\" -}}{{- end -}}")
f(tmpl1, tmpl2, false)
}

View File

@@ -1,554 +1,95 @@
// Copyright 2013 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package templates
import (
"errors"
"bytes"
"fmt"
htmlTpl "html/template"
"io"
"math"
"net"
"net/url"
"regexp"
"sort"
"strconv"
"strings"
"sync"
textTpl "text/template"
"time"
"github.com/bmatcuk/doublestar/v4"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/datasource"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/formatutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promutils"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
)
// go template execution fails when it's tree is empty
const defaultTemplate = `{{- define "default.template" -}}{{- end -}}`
// supported variables are list in https://docs.victoriametrics.com/vmalert/#templating.
const tplHeaders = `{{ $value := .Value }}{{ $labels := .Labels }}{{ $expr := .Expr }}{{ $externalLabels := .ExternalLabels }}{{ $externalURL := .ExternalURL }}{{ $alertID := .AlertID }}{{ $groupID := .GroupID }}{{ $activeAt := .ActiveAt }}{{ $for := .For }}`
var tplMu sync.RWMutex
type textTemplate struct {
current *textTpl.Template
replacement *textTpl.Template
// AlertTplData is used to execute templating
type AlertTplData struct {
Labels map[string]string
Value float64
Expr string
AlertID uint64
GroupID uint64
ActiveAt time.Time
For time.Duration
}
var masterTmpl textTemplate
func newTemplate() *textTpl.Template {
tmpl := textTpl.New("").Option("missingkey=zero").Funcs(templateFuncs())
return textTpl.Must(tmpl.Parse(defaultTemplate))
}
// Load func loads templates from multiple globs specified in pathPatterns and either
// sets them directly to current template if it's the first init;
// or sets replacement templates and wait for Reload() to replace current template with replacement.
func Load(pathPatterns []string, externalURL url.URL) error {
tmpl := newTemplate()
for _, tp := range pathPatterns {
p, err := doublestar.FilepathGlob(tp)
// ValidateTemplates validates the given annotations,
// mock the `query` function during validation.
func ValidateTemplates(annotations map[string]string) error {
// it's ok to reuse one template for multiple text validations.
tmpl := GetCurrentTmpl()
tmpl = tmpl.Funcs(FuncsWithQuery(nil))
for _, v := range annotations {
_, err := tmpl.Parse(tplHeaders + v)
if err != nil {
return fmt.Errorf("failed to retrieve a template glob %q: %w", tp, err)
return fmt.Errorf("failed to parse text %q into template: %w", v, err)
}
if len(p) > 0 {
tmpl, err = tmpl.ParseFiles(p...)
if err != nil {
return fmt.Errorf("failed to parse template glob %q: %w", tp, err)
}
}
}
if len(tmpl.Templates()) > 0 {
err := tmpl.Execute(io.Discard, nil)
if err != nil {
return fmt.Errorf("failed to execute template: %w", err)
}
}
tplMu.Lock()
defer tplMu.Unlock()
tmpl = tmpl.Funcs(funcsWithExternalURL(externalURL))
if masterTmpl.current == nil {
masterTmpl.current = tmpl
} else {
masterTmpl.replacement = tmpl
}
return nil
}
// Reload func replaces current template with a replacement template
// which was set by Load with override=false
func Reload() {
tplMu.Lock()
defer tplMu.Unlock()
if masterTmpl.replacement != nil {
masterTmpl.current = masterTmpl.replacement
masterTmpl.replacement = nil
}
}
// metric is private copy of datasource.Metric,
// it is used for templating annotations,
// Labels as map simplifies templates evaluation.
type metric struct {
Labels map[string]string
Timestamp int64
Value float64
}
// datasourceMetricsToTemplateMetrics converts Metrics from datasource package to private copy for templating.
func datasourceMetricsToTemplateMetrics(ms []datasource.Metric) []metric {
mss := make([]metric, 0, len(ms))
for _, m := range ms {
labelsMap := make(map[string]string, len(m.Labels))
for _, labelValue := range m.Labels {
labelsMap[labelValue.Name] = labelValue.Value
}
mss = append(mss, metric{
Labels: labelsMap,
Timestamp: m.Timestamps[0],
Value: m.Values[0]})
}
return mss
}
// QueryFn is used to wrap a call to datasource into simple-to-use function
// for templating functions.
type QueryFn func(query string) ([]datasource.Metric, error)
// GetWithFuncs returns a copy of current template with additional FuncMap
// provided with funcs argument
func GetWithFuncs(funcs textTpl.FuncMap) (*textTpl.Template, error) {
// GetCurrentTmpl returns a copy of the current global template
func GetCurrentTmpl() *textTpl.Template {
tplMu.RLock()
defer tplMu.RUnlock()
tmpl, err := masterTmpl.current.Clone()
tmpl, err := masterTmpl.Clone()
if err != nil {
return nil, err
logger.Panicf("failed to clone current rule template: %w", err)
}
// Clone() doesn't copy tpl Options, so we set them manually
tmpl = tmpl.Option("missingkey=zero")
return tmpl.Funcs(funcs), nil
return tmpl
}
// FuncsWithQuery returns a function map that depends on metric data
func FuncsWithQuery(query QueryFn) textTpl.FuncMap {
return textTpl.FuncMap{
"query": func(q string) ([]metric, error) {
if query == nil {
return nil, fmt.Errorf("cannot execute query %q: query is not available in this context", q)
}
type tplData struct {
AlertTplData
ExternalLabels map[string]string
ExternalURL string
}
result, err := query(q)
if err != nil {
return nil, err
}
return datasourceMetricsToTemplateMetrics(result), nil
},
// ParseWithFixedHeader parses the text with the fixed tplHeaders into the given template
func ParseWithFixedHeader(text string, tpl *textTpl.Template) (*textTpl.Template, error) {
return tpl.Parse(tplHeaders + text)
}
// ExecuteWithoutTemplate retrieves the current global templates, parses the text and executes with the given data
func ExecuteWithoutTemplate(q QueryFn, text string, data AlertTplData) (string, error) {
if !strings.Contains(text, "{{") || !strings.Contains(text, "}}") {
return text, nil
}
}
// funcsWithExternalURL returns a function map that depends on externalURL value
func funcsWithExternalURL(externalURL url.URL) textTpl.FuncMap {
return textTpl.FuncMap{
"externalURL": func() string {
return externalURL.String()
},
var err error
tmpl := GetCurrentTmpl()
tmpl = tmpl.Funcs(FuncsWithQuery(q))
tmpl, err = tmpl.Parse(tplHeaders + text)
if err != nil {
return "", fmt.Errorf("failed to parse text %q into template: %w", text, err)
"pathPrefix": func() string {
return externalURL.Path
},
}
return ExecuteWithTemplate(data, tmpl)
}
// templateFuncs initiates template helper functions
func templateFuncs() textTpl.FuncMap {
// See https://prometheus.io/docs/prometheus/latest/configuration/template_reference/
// and https://github.com/prometheus/prometheus/blob/fa6e05903fd3ce52e374a6e1bf4eb98c9f1f45a7/template/template.go#L150
return textTpl.FuncMap{
/* Strings */
// title returns a copy of the string s with all Unicode letters
// that begin words mapped to their Unicode title case.
// alias for https://golang.org/pkg/strings/#Title
"title": strings.Title,
// toUpper returns s with all Unicode letters mapped to their upper case.
// alias for https://golang.org/pkg/strings/#ToUpper
"toUpper": strings.ToUpper,
// toLower returns s with all Unicode letters mapped to their lower case.
// alias for https://golang.org/pkg/strings/#ToLower
"toLower": strings.ToLower,
// crlfEscape replaces '\n' and '\r' chars with `\\n` and `\\r`.
// This function is deprecated.
//
// It is better to use quotesEscape, jsonEscape, queryEscape or pathEscape instead -
// these functions properly escape `\n` and `\r` chars according to their purpose.
"crlfEscape": func(q string) string {
q = strings.Replace(q, "\n", `\n`, -1)
return strings.Replace(q, "\r", `\r`, -1)
},
// quotesEscape escapes the string, so it can be safely put inside JSON string.
//
// See also jsonEscape.
"quotesEscape": quotesEscape,
// jsonEscape converts the string to properly encoded JSON string.
//
// See also quotesEscape.
"jsonEscape": jsonEscape,
// htmlEscape applies html-escaping to q, so it can be safely embedded as plaintext into html.
//
// See also safeHtml.
"htmlEscape": htmlEscape,
// stripPort splits string into host and port, then returns only host.
"stripPort": func(hostPort string) string {
host, _, err := net.SplitHostPort(hostPort)
if err != nil {
return hostPort
}
return host
},
// stripDomain removes the domain part of a FQDN. Leaves port untouched.
"stripDomain": func(hostPort string) string {
host, port, err := net.SplitHostPort(hostPort)
if err != nil {
host = hostPort
}
ip := net.ParseIP(host)
if ip != nil {
return hostPort
}
host = strings.Split(host, ".")[0]
if port != "" {
return net.JoinHostPort(host, port)
}
return host
},
// match reports whether the string s
// contains any match of the regular expression pattern.
// alias for https://golang.org/pkg/regexp/#MatchString
"match": regexp.MatchString,
// reReplaceAll ReplaceAllString returns a copy of src, replacing matches of the Regexp with
// the replacement string repl. Inside repl, $ signs are interpreted as in Expand,
// so for instance $1 represents the text of the first submatch.
// alias for https://golang.org/pkg/regexp/#Regexp.ReplaceAllString
"reReplaceAll": func(pattern, repl, text string) string {
re := regexp.MustCompile(pattern)
return re.ReplaceAllString(text, repl)
},
// parseDuration parses a duration string such as "1h" into the number of seconds it represents
"parseDuration": func(s string) (float64, error) {
d, err := promutils.ParseDuration(s)
if err != nil {
return 0, err
}
return d.Seconds(), nil
},
// same with parseDuration but returns a time.Duration
"parseDurationTime": func(s string) (time.Duration, error) {
d, err := promutils.ParseDuration(s)
if err != nil {
return 0, err
}
return d, nil
},
/* Numbers */
// humanize converts given number to a human readable format
// by adding metric prefixes https://en.wikipedia.org/wiki/Metric_prefix
"humanize": func(i any) (string, error) {
v, err := toFloat64(i)
if err != nil {
return "", err
}
if v == 0 || math.IsNaN(v) || math.IsInf(v, 0) {
return fmt.Sprintf("%.4g", v), nil
}
if math.Abs(v) >= 1 {
prefix := ""
for _, p := range []string{"k", "M", "G", "T", "P", "E", "Z", "Y"} {
if math.Abs(v) < 1000 {
break
}
prefix = p
v /= 1000
}
return fmt.Sprintf("%.4g%s", v, prefix), nil
}
prefix := ""
for _, p := range []string{"m", "u", "n", "p", "f", "a", "z", "y"} {
if math.Abs(v) >= 1 {
break
}
prefix = p
v *= 1000
}
return fmt.Sprintf("%.4g%s", v, prefix), nil
},
// humanize1024 converts given number to a human readable format with 1024 as base
"humanize1024": func(i any) (string, error) {
v, err := toFloat64(i)
if err != nil {
return "", err
}
if math.Abs(v) <= 1 || math.IsNaN(v) || math.IsInf(v, 0) {
return fmt.Sprintf("%.4g", v), nil
}
return formatutil.HumanizeBytes(v), nil
},
// humanizeDuration converts given seconds to a human-readable duration
"humanizeDuration": func(i any) (string, error) {
v, err := toFloat64(i)
if err != nil {
return "", err
}
if math.IsNaN(v) || math.IsInf(v, 0) {
return fmt.Sprintf("%.4g", v), nil
}
if v == 0 {
return fmt.Sprintf("%.4gs", v), nil
}
if math.Abs(v) >= 1 {
sign := ""
if v < 0 {
sign = "-"
v = -v
}
seconds := int64(v) % 60
minutes := (int64(v) / 60) % 60
hours := (int64(v) / 60 / 60) % 24
days := int64(v) / 60 / 60 / 24
// For days to minutes, we display seconds as an integer.
if days != 0 {
return fmt.Sprintf("%s%dd %dh %dm %ds", sign, days, hours, minutes, seconds), nil
}
if hours != 0 {
return fmt.Sprintf("%s%dh %dm %ds", sign, hours, minutes, seconds), nil
}
if minutes != 0 {
return fmt.Sprintf("%s%dm %ds", sign, minutes, seconds), nil
}
// For seconds, we display 4 significant digits.
return fmt.Sprintf("%s%.4gs", sign, v), nil
}
prefix := ""
for _, p := range []string{"m", "u", "n", "p", "f", "a", "z", "y"} {
if math.Abs(v) >= 1 {
break
}
prefix = p
v *= 1000
}
return fmt.Sprintf("%.4g%ss", v, prefix), nil
},
// humanizePercentage converts given ratio value to a fraction of 100
"humanizePercentage": func(i any) (string, error) {
v, err := toFloat64(i)
if err != nil {
return "", err
}
return fmt.Sprintf("%.4g%%", v*100), nil
},
// humanizeTimestamp converts given timestamp to a human readable time equivalent
"humanizeTimestamp": func(i any) (string, error) {
v, err := toFloat64(i)
if err != nil {
return "", err
}
if math.IsNaN(v) || math.IsInf(v, 0) {
return fmt.Sprintf("%.4g", v), nil
}
t := timeFromUnixTimestamp(v).Time().UTC()
return fmt.Sprint(t), nil
},
// toTime converts given timestamp to a time.Time.
"toTime": func(i any) (time.Time, error) {
v, err := toFloat64(i)
if err != nil {
return time.Time{}, err
}
if math.IsNaN(v) || math.IsInf(v, 0) {
return time.Time{}, fmt.Errorf("cannot convert %v to time.Time", v)
}
t := timeFromUnixTimestamp(v).Time().UTC()
return t, nil
},
/* URLs */
// externalURL returns value of `external.url` flag
"externalURL": func() string {
// externalURL function supposed to be substituted at FuncsWithExteralURL().
// it is present here only for validation purposes, when there is no
// provided datasource.
//
// return non-empty slice to pass validation with chained functions in template
return ""
},
// pathPrefix returns a Path segment from the URL value in `external.url` flag
"pathPrefix": func() string {
// pathPrefix function supposed to be substituted at FuncsWithExteralURL().
// it is present here only for validation purposes, when there is no
// provided datasource.
//
// return non-empty slice to pass validation with chained functions in template
return ""
},
// pathEscape escapes the string so it can be safely placed inside a URL path segment.
//
// See also queryEscape.
"pathEscape": url.PathEscape,
// queryEscape escapes the string so it can be safely placed inside a query arg in URL.
//
// See also queryEscape.
"queryEscape": url.QueryEscape,
// query executes the MetricsQL/PromQL query against
// configured `datasource.url` address.
// For example, {{ query "foo" | first | value }} will
// execute "/api/v1/query?query=foo" request and will return
// the first value in response.
"query": func(_ string) ([]metric, error) {
// query function supposed to be substituted at FuncsWithQuery().
// it is present here only for validation purposes, when there is no
// provided datasource.
//
// return non-empty slice to pass validation with chained functions in template
// see issue #989 for details
return []metric{{}}, nil
},
// first returns the first by order element from the given metrics list.
// usually used alongside with `query` template function.
"first": func(metrics []metric) (metric, error) {
if len(metrics) > 0 {
return metrics[0], nil
}
return metric{}, errors.New("first() called on vector with no elements")
},
// label returns the value of the given label name for the given metric.
// usually used alongside with `query` template function.
"label": func(label string, m metric) string {
return m.Labels[label]
},
// value returns the value of the given metric.
// usually used alongside with `query` template function.
"value": func(m metric) float64 {
return m.Value
},
// strvalue returns metric name.
"strvalue": func(m metric) string {
return m.Labels["__name__"]
},
// sortByLabel sorts the given metrics by provided label key
"sortByLabel": func(label string, metrics []metric) []metric {
sort.SliceStable(metrics, func(i, j int) bool {
return metrics[i].Labels[label] < metrics[j].Labels[label]
})
return metrics
},
/* Helpers */
// Converts a list of objects to a map with keys arg0, arg1 etc.
// This is intended to allow multiple arguments to be passed to templates.
"args": func(args ...any) map[string]any {
result := make(map[string]any)
for i, a := range args {
result[fmt.Sprintf("arg%d", i)] = a
}
return result
},
// safeHtml marks string as HTML not requiring auto-escaping.
//
// See also htmlEscape.
"safeHtml": func(text string) htmlTpl.HTML {
return htmlTpl.HTML(text)
},
// ExecuteWithTemplate executes with the given template and data
func ExecuteWithTemplate(data AlertTplData, tpl *textTpl.Template) (string, error) {
fullData := tplData{
data,
externalLabels,
externalURL.String(),
}
}
// Time is the number of milliseconds since the epoch
// (1970-01-01 00:00 UTC) excluding leap seconds.
type Time int64
// timeFromUnixTimestamp returns the Time equivalent to t in unix timestamp.
func timeFromUnixTimestamp(t float64) Time {
return Time(t * 1e3)
}
// The number of nanoseconds per minimum tick.
const nanosPerTick = int64(minimumTick / time.Nanosecond)
// MinimumTick is the minimum supported time resolution. This has to be
// at least time.Second in order for the code below to work.
const minimumTick = time.Millisecond
// second is the Time duration equivalent to one second.
const second = int64(time.Second / minimumTick)
// Time returns the time.Time representation of t.
func (t Time) Time() time.Time {
return time.Unix(int64(t)/second, (int64(t)%second)*nanosPerTick)
}
func toFloat64(v any) (float64, error) {
switch i := v.(type) {
case float64:
return i, nil
case float32:
return float64(i), nil
case int64:
return float64(i), nil
case int32:
return float64(i), nil
case int:
return float64(i), nil
case uint64:
return float64(i), nil
case uint32:
return float64(i), nil
case uint:
return float64(i), nil
case string:
return strconv.ParseFloat(i, 64)
default:
return 0, fmt.Errorf("unexpected value type %v", i)
var buf bytes.Buffer
// returns the zero value for the map type's element
tpl.Option("missingkey=zero")
if err := tpl.Execute(&buf, fullData); err != nil {
return "", fmt.Errorf("failed to execute template: %w", err)
}
return buf.String(), nil
}

View File

@@ -1,239 +1,226 @@
package templates
import (
"math"
"fmt"
"net/url"
"strings"
"testing"
textTpl "text/template"
"time"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/datasource"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
)
func TestTemplateFuncs_StringConversion(t *testing.T) {
f := func(funcName, s, resultExpected string) {
func TestValidateTemplates(t *testing.T) {
f := func(annotations map[string]string, isValid bool) {
t.Helper()
funcs := templateFuncs()
v := funcs[funcName]
fLocal := v.(func(s string) string)
result := fLocal(s)
if result != resultExpected {
t.Fatalf("unexpected result for %s(%q); got\n%s\nwant\n%s", funcName, s, result, resultExpected)
err := ValidateTemplates(annotations)
if (err == nil) != isValid {
t.Fatalf("failed to validate template, got %t; want %t", (err == nil), isValid)
}
}
f("title", "foo bar", "Foo Bar")
f("toUpper", "foo", "FOO")
f("toLower", "FOO", "foo")
f("pathEscape", "foo/bar\n+baz", "foo%2Fbar%0A+baz")
f("queryEscape", "foo+bar\n+baz", "foo%2Bbar%0A%2Bbaz")
f("jsonEscape", `foo{bar="baz"}`+"\n + 1", `"foo{bar=\"baz\"}\n + 1"`)
f("quotesEscape", `foo{bar="baz"}`+"\n + 1", `foo{bar=\"baz\"}\n + 1`)
f("htmlEscape", "foo < 10\nabc", "foo &lt; 10\nabc")
f("crlfEscape", "foo\nbar\rx", `foo\nbar\rx`)
f("stripPort", "foo", "foo")
f("stripPort", "foo:1234", "foo")
f("stripDomain", "foo.bar.baz", "foo")
f("stripDomain", "foo.bar:123", "foo:123")
// empty
f(map[string]string{}, true)
// wrong text
f(map[string]string{
"summary": "{{",
}, false)
// valid
f(map[string]string{
"value": "{{$value}}",
"summary": "it's a test summary",
}, true)
// invalid variable
f(map[string]string{
"value": "{{$invalidValue}}",
"summary": "it's a test summary",
}, false)
}
func TestTemplateFuncs_Match(t *testing.T) {
funcs := templateFuncs()
// check "match" func
matchFunc := funcs["match"].(func(pattern, s string) (bool, error))
if _, err := matchFunc("invalid[regexp", "abc"); err == nil {
t.Fatalf("expecting non-nil error on invalid regexp")
}
ok, err := matchFunc("abc", "def")
func TestExecuteWithoutTemplate(t *testing.T) {
extLabels := make(map[string]string)
const (
extCluster = "prod"
extDC = "east"
extURL = "https://foo.bar"
)
url, _ := url.Parse(extURL)
extLabels["cluster"] = extCluster
extLabels["dc"] = extDC
err := Init(nil, extLabels, *url)
if err != nil {
t.Fatalf("unexpected error")
t.Fatalf("cannot init templates: %s", err)
}
if ok {
t.Fatalf("unexpected match")
}
ok, err = matchFunc("a.+b", "acsdb")
if err != nil {
t.Fatalf("unexpected error")
}
if !ok {
t.Fatalf("unexpected mismatch")
}
}
func TestTemplateFuncs_Formatting(t *testing.T) {
f := func(funcName string, p any, resultExpected string) {
f := func(data AlertTplData, annotations, expResults map[string]string) {
t.Helper()
funcs := templateFuncs()
v := funcs[funcName]
fLocal := v.(func(s any) (string, error))
result, err := fLocal(p)
if err != nil {
t.Fatalf("unexpected error for %s(%f): %s", funcName, p, err)
qFn := func(_ string) ([]datasource.Metric, error) {
return []datasource.Metric{
{
Labels: []prompbmarshal.Label{
{Name: "foo", Value: "bar"},
{Name: "baz", Value: "qux"},
},
Values: []float64{1},
Timestamps: []int64{1},
},
{
Labels: []prompbmarshal.Label{
{Name: "foo", Value: "garply"},
{Name: "baz", Value: "fred"},
},
Values: []float64{2},
Timestamps: []int64{1},
},
}, nil
}
if result != resultExpected {
t.Fatalf("unexpected result for %s(%f); got\n%s\nwant\n%s", funcName, p, result, resultExpected)
}
}
f("humanize1024", float64(0), "0")
f("humanize1024", math.Inf(0), "+Inf")
f("humanize1024", math.NaN(), "NaN")
f("humanize1024", float64(127087), "124.1ki")
f("humanize1024", float64(130137088), "124.1Mi")
f("humanize1024", float64(133260378112), "124.1Gi")
f("humanize1024", float64(136458627186688), "124.1Ti")
f("humanize1024", float64(139733634239168512), "124.1Pi")
f("humanize1024", float64(143087241460908556288), "124.1Ei")
f("humanize1024", float64(146521335255970361638912), "124.1Zi")
f("humanize1024", float64(150037847302113650318245888), "124.1Yi")
f("humanize1024", float64(153638755637364377925883789312), "1.271e+05Yi")
f("humanize", float64(127087), "127.1k")
f("humanize", float64(136458627186688), "136.5T")
f("humanizeDuration", 1, "1s")
f("humanizeDuration", 0.2, "200ms")
f("humanizeDuration", 42000, "11h 40m 0s")
f("humanizeDuration", 16790555, "194d 8h 2m 35s")
f("humanizePercentage", 1, "100%")
f("humanizePercentage", 0.8, "80%")
f("humanizePercentage", 0.015, "1.5%")
f("humanizeTimestamp", 1679055557, "2023-03-17 12:19:17 +0000 UTC")
}
func mkTemplate(current, replacement any) textTemplate {
tmpl := textTemplate{}
if current != nil {
switch val := current.(type) {
case string:
tmpl.current = textTpl.Must(newTemplate().Parse(val))
}
}
if replacement != nil {
switch val := replacement.(type) {
case string:
tmpl.replacement = textTpl.Must(newTemplate().Parse(val))
}
}
return tmpl
}
func equalTemplates(tmpls ...*textTpl.Template) bool {
var cmp *textTpl.Template
for i, tmpl := range tmpls {
if i == 0 {
cmp = tmpl
} else {
if cmp == nil || tmpl == nil {
if cmp != tmpl {
return false
}
continue
for k := range annotations {
v, err := ExecuteWithoutTemplate(qFn, annotations[k], data)
if err != nil {
t.Fatalf("cannot execute template: %s", err)
}
if len(tmpl.Templates()) != len(cmp.Templates()) {
return false
}
for _, t := range tmpl.Templates() {
tp := cmp.Lookup(t.Name())
if tp == nil {
return false
}
if tp.Root.String() != t.Root.String() {
return false
}
if v != expResults[k] {
t.Fatalf("unexpected result; got %s; want %s", v, expResults[k])
}
}
}
return true
}
func TestTemplatesLoad_Failure(t *testing.T) {
f := func(pathPatterns []string, expectedErrStr string) {
t.Helper()
err := Load(pathPatterns, url.URL{})
if err == nil {
t.Fatalf("expecting non-nil error")
}
errStr := err.Error()
if !strings.Contains(errStr, expectedErrStr) {
t.Fatalf("the returned error %q doesn't contain %q", errStr, expectedErrStr)
}
}
// load template with syntax error
f([]string{
"templates/other/nested/bad0-*.tpl",
"templates/test/good0-*.tpl",
}, "failed to parse template glob")
}
func TestTemplatesLoad_Success(t *testing.T) {
f := func(pathPatterns []string, expectedTmpl textTemplate) {
t.Helper()
masterTmplOrig := masterTmpl
defer func() {
masterTmpl = masterTmplOrig
}()
if err := Load(pathPatterns, url.URL{}); err != nil {
t.Fatalf("cannot load templates: %s", err)
}
Reload()
if !equalTemplates(masterTmpl.replacement, expectedTmpl.replacement) {
t.Fatalf("unexpected replacement template\ngot\n%+v\nwant\n%+v", masterTmpl.replacement, expectedTmpl.replacement)
}
if !equalTemplates(masterTmpl.current, expectedTmpl.current) {
t.Fatalf("unexpected current template\ngot\n%+v\nwant\n%+v", masterTmpl.current, expectedTmpl.current)
}
}
// non existing path
pathPatterns := []string{
"templates/non-existing/good-*.tpl",
"templates/absent/good-*.tpl",
}
expectedTmpl := mkTemplate(``, nil)
f(pathPatterns, expectedTmpl)
// existing path
pathPatterns = []string{
"templates/test/good0-*.tpl",
}
expectedTmpl = mkTemplate(`
{{- define "good0-test.tpl" -}}{{- end -}}
{{- define "test.0" -}}
{{ printf "Hello %s!" externalURL }}
{{- end -}}
{{- define "test.2" -}}
{{ printf "Hello %s!" externalURL }}
{{- end -}}
{{- define "test.3" -}}
{{ printf "Hello %s!" externalURL }}
{{- end -}}
`, nil)
f(pathPatterns, expectedTmpl)
// existing path defined template override
pathPatterns = []string{
"templates/other/nested/good0-*.tpl",
}
expectedTmpl = mkTemplate(`
{{- define "good0-test.tpl" -}}{{- end -}}
{{- define "test.0" -}}
{{ printf "Hello %s!" externalURL }}
{{- end -}}
{{- define "test.1" -}}
{{ printf "Hello %s!" externalURL }}
{{- end -}}
{{- define "test.3" -}}
{{ printf "Hello %s!" externalURL }}
{{- end -}}
`, nil)
f(pathPatterns, expectedTmpl)
// empty-alert
f(AlertTplData{}, map[string]string{}, map[string]string{})
// no-template
f(AlertTplData{
Value: 1e4,
Labels: map[string]string{
"instance": "localhost",
},
}, map[string]string{
"summary": "it's a test summary",
"description": "it's a test description",
}, map[string]string{
"summary": "it's a test summary",
"description": "it's a test description",
})
// label-template
f(AlertTplData{
Value: 1e4,
Labels: map[string]string{
"job": "staging",
"instance": "localhost",
},
For: 5 * time.Minute,
}, map[string]string{
"summary": "Too high connection number for {{$labels.instance}} for job {{$labels.job}}",
"description": "It is {{ $value }} connections for {{$labels.instance}} for more than {{ .For }}",
"non-existing-label": "{{$labels.nonexisting}}",
}, map[string]string{
"summary": "Too high connection number for localhost for job staging",
"description": "It is 10000 connections for localhost for more than 5m0s",
"non-existing-label": "",
})
// label template override
f(AlertTplData{
Value: 1e4,
}, map[string]string{
"summary": `{{- define "default.template" -}} {{ printf "summary" }} {{- end -}} {{ template "default.template" . }}`,
"description": `{{- define "default.template" -}} {{ printf "description" }} {{- end -}} {{ template "default.template" . }}`,
"value": `{{$value }}`,
}, map[string]string{
"summary": "summary",
"description": "description",
"value": "10000",
})
// expression-template
f(AlertTplData{
Expr: `vm_rows{"label"="bar"}<0`,
}, map[string]string{
"exprEscapedQuery": "{{ $expr|queryEscape }}",
"exprEscapedPath": "{{ $expr|pathEscape }}",
"exprEscapedJSON": "{{ $expr|jsonEscape }}",
"exprEscapedQuotes": "{{ $expr|quotesEscape }}",
"exprEscapedHTML": "{{ $expr|htmlEscape }}",
}, map[string]string{
"exprEscapedQuery": "vm_rows%7B%22label%22%3D%22bar%22%7D%3C0",
"exprEscapedPath": "vm_rows%7B%22label%22=%22bar%22%7D%3C0",
"exprEscapedJSON": `"vm_rows{\"label\"=\"bar\"}\u003c0"`,
"exprEscapedQuotes": `vm_rows{\"label\"=\"bar\"}\u003c0`,
"exprEscapedHTML": "vm_rows{&quot;label&quot;=&quot;bar&quot;}&lt;0",
})
// query function
f(AlertTplData{
Expr: `vm_rows{"label"="bar"}>0`,
}, map[string]string{
"summary": `{{ query "foo" | first | value }}`,
"desc": `{{ range query "bar" }}{{ . | label "foo" }} {{ . | value }};{{ end }}`,
}, map[string]string{
"summary": "1",
"desc": "bar 1;garply 2;",
})
// external
f(AlertTplData{
Value: 1e4,
Labels: map[string]string{
"job": "staging",
"instance": "localhost",
},
}, map[string]string{
"url": "{{ $externalURL }}",
"summary": "Issues with {{$labels.instance}} (dc-{{$externalLabels.dc}}) for job {{$labels.job}}",
"description": "It is {{ $value }} connections for {{$labels.instance}} (cluster-{{$externalLabels.cluster}})",
}, map[string]string{
"url": extURL,
"summary": fmt.Sprintf("Issues with localhost (dc-%s) for job staging", extDC),
"description": fmt.Sprintf("It is 10000 connections for localhost (cluster-%s)", extCluster),
})
// alert, group IDs & ActiveAt time
f(AlertTplData{
AlertID: 42,
GroupID: 24,
ActiveAt: time.Date(2022, 8, 19, 20, 34, 58, 651387237, time.UTC),
}, map[string]string{
"url": "/api/v1/alert?alertID={{$alertID}}&groupID={{$groupID}}",
"diagram": "![](http://example.com?render={{$activeAt.Unix}}",
}, map[string]string{
"url": "/api/v1/alert?alertID=42&groupID=24",
"diagram": "![](http://example.com?render=1660941298",
})
// ActiveAt time is nil
f(AlertTplData{}, map[string]string{
"default_time": "{{$activeAt}}",
}, map[string]string{
"default_time": "0001-01-01 00:00:00 +0000 UTC",
})
// ActiveAt custom format
f(AlertTplData{
ActiveAt: time.Date(2022, 8, 19, 20, 34, 58, 651387237, time.UTC),
}, map[string]string{
"fire_time": `{{$activeAt.Format "2006/01/02 15:04:05"}}`,
}, map[string]string{
"fire_time": "2022/08/19 20:34:58",
})
// ActiveAt query range
f(AlertTplData{
ActiveAt: time.Date(2022, 8, 19, 20, 34, 58, 651387237, time.UTC),
}, map[string]string{
"grafana_url": `vm-grafana.com?from={{($activeAt.Add (parseDurationTime "1h")).Unix}}&to={{($activeAt.Add (parseDurationTime "-1h")).Unix}}`,
}, map[string]string{
"grafana_url": "vm-grafana.com?from=1660944898&to=1660937698",
})
}

View File

@@ -222,7 +222,8 @@ func processRequest(w http.ResponseWriter, r *http.Request, ui *UserInfo) {
isDefault = true
}
rtb := newReadTrackingBody(r.Body, maxRequestBodySizeToRetry.IntN())
rtb := getReadTrackingBody(r.Body, maxRequestBodySizeToRetry.IntN())
defer putReadTrackingBody(rtb)
r.Body = rtb
maxAttempts := up.getBackendsCount()
@@ -558,11 +559,22 @@ type readTrackingBody struct {
bufComplete bool
}
func newReadTrackingBody(r io.ReadCloser, maxBodySize int) *readTrackingBody {
// do not use sync.Pool there
// since http.RoundTrip may still use request body after return
// See this issue for details https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8051
rtb := &readTrackingBody{}
func (rtb *readTrackingBody) reset() {
rtb.maxBodySize = 0
rtb.r = nil
rtb.buf = rtb.buf[:0]
rtb.readBuf = nil
rtb.cannotRetry = false
rtb.bufComplete = false
}
func getReadTrackingBody(r io.ReadCloser, maxBodySize int) *readTrackingBody {
v := readTrackingBodyPool.Get()
if v == nil {
v = &readTrackingBody{}
}
rtb := v.(*readTrackingBody)
if maxBodySize < 0 {
maxBodySize = 0
}
@@ -585,6 +597,13 @@ func (r *zeroReader) Close() error {
return nil
}
func putReadTrackingBody(rtb *readTrackingBody) {
rtb.reset()
readTrackingBodyPool.Put(rtb)
}
var readTrackingBodyPool sync.Pool
// Read implements io.Reader interface.
func (rtb *readTrackingBody) Read(p []byte) (int, error) {
if len(rtb.readBuf) > 0 {

View File

@@ -195,7 +195,7 @@ unauthorized_user:
}
responseExpected = `
statusCode=401
Expected to receive non-empty authKey when -reloadAuthKey is set`
The provided authKey doesn't match -reloadAuthKey`
f(cfgStr, requestURL, backendHandler, responseExpected)
if err := reloadAuthKey.Set(origAuthKey); err != nil {
t.Fatalf("unexpected error: %s", err)
@@ -545,7 +545,8 @@ func TestReadTrackingBody_RetrySuccess(t *testing.T) {
f := func(s string, maxBodySize int) {
t.Helper()
rtb := newReadTrackingBody(io.NopCloser(bytes.NewBufferString(s)), maxBodySize)
rtb := getReadTrackingBody(io.NopCloser(bytes.NewBufferString(s)), maxBodySize)
defer putReadTrackingBody(rtb)
if !rtb.canRetry() {
t.Fatalf("canRetry() must return true before reading anything")
@@ -580,7 +581,8 @@ func TestReadTrackingBody_RetrySuccessPartialRead(t *testing.T) {
t.Helper()
// Check the case with partial read
rtb := newReadTrackingBody(io.NopCloser(bytes.NewBufferString(s)), maxBodySize)
rtb := getReadTrackingBody(io.NopCloser(bytes.NewBufferString(s)), maxBodySize)
defer putReadTrackingBody(rtb)
for i := 0; i < len(s); i++ {
buf := make([]byte, i)
@@ -629,7 +631,8 @@ func TestReadTrackingBody_RetryFailureTooBigBody(t *testing.T) {
f := func(s string, maxBodySize int) {
t.Helper()
rtb := newReadTrackingBody(io.NopCloser(bytes.NewBufferString(s)), maxBodySize)
rtb := getReadTrackingBody(io.NopCloser(bytes.NewBufferString(s)), maxBodySize)
defer putReadTrackingBody(rtb)
if !rtb.canRetry() {
t.Fatalf("canRetry() must return true before reading anything")
@@ -678,7 +681,8 @@ func TestReadTrackingBody_RetryFailureZeroOrNegativeMaxBodySize(t *testing.T) {
f := func(s string, maxBodySize int) {
t.Helper()
rtb := newReadTrackingBody(io.NopCloser(bytes.NewBufferString(s)), maxBodySize)
rtb := getReadTrackingBody(io.NopCloser(bytes.NewBufferString(s)), maxBodySize)
defer putReadTrackingBody(rtb)
if !rtb.canRetry() {
t.Fatalf("canRetry() must return true before reading anything")

View File

@@ -40,15 +40,15 @@ type filter struct {
labelValue string
}
func (f filter) inRange(minV, maxV int64) bool {
func (f filter) inRange(min, max int64) bool {
fmin, fmax := f.min, f.max
if minV == 0 {
fmin = minV
if min == 0 {
fmin = min
}
if fmax == 0 {
fmax = maxV
fmax = max
}
return minV <= fmax && fmin <= maxV
return min <= fmax && fmin <= max
}
// NewClient creates and validates new Client
@@ -59,13 +59,13 @@ func NewClient(cfg Config) (*Client, error) {
return nil, fmt.Errorf("failed to open snapshot %q: %s", cfg.Snapshot, err)
}
c := &Client{DBReadOnly: db}
minTime, maxTime, err := parseTime(cfg.Filter.TimeMin, cfg.Filter.TimeMax)
min, max, err := parseTime(cfg.Filter.TimeMin, cfg.Filter.TimeMax)
if err != nil {
return nil, fmt.Errorf("failed to parse time in filter: %s", err)
}
c.filter = filter{
min: minTime,
max: maxTime,
min: min,
max: max,
label: cfg.Filter.Label,
labelValue: cfg.Filter.LabelValue,
}

View File

@@ -98,13 +98,13 @@ func aggrMin(values []float64) float64 {
if pos < 0 {
return nan
}
minV := values[pos]
min := values[pos]
for _, v := range values[pos+1:] {
if !math.IsNaN(v) && v < minV {
minV = v
if !math.IsNaN(v) && v < min {
min = v
}
}
return minV
return min
}
func aggrMax(values []float64) float64 {
@@ -112,13 +112,13 @@ func aggrMax(values []float64) float64 {
if pos < 0 {
return nan
}
maxV := values[pos]
max := values[pos]
for _, v := range values[pos+1:] {
if !math.IsNaN(v) && v > maxV {
maxV = v
if !math.IsNaN(v) && v > max {
max = v
}
}
return maxV
return max
}
func aggrDiff(values []float64) float64 {
@@ -177,12 +177,12 @@ func aggrCount(values []float64) float64 {
}
func aggrRange(values []float64) float64 {
minV := aggrMin(values)
if math.IsNaN(minV) {
min := aggrMin(values)
if math.IsNaN(min) {
return nan
}
maxV := aggrMax(values)
return maxV - minV
max := aggrMax(values)
return max - min
}
func aggrMultiply(values []float64) float64 {

View File

@@ -2594,17 +2594,17 @@ func transformMinMax(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, e
}
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
values := s.Values
minV := aggrMin(values)
if math.IsNaN(minV) {
minV = 0
min := aggrMin(values)
if math.IsNaN(min) {
min = 0
}
maxV := aggrMax(values)
if math.IsNaN(maxV) {
maxV = 0
max := aggrMax(values)
if math.IsNaN(max) {
max = 0
}
vRange := maxV - minV
vRange := max - min
for i, v := range values {
v = (v - minV) / vRange
v = (v - min) / vRange
if math.IsInf(v, 0) {
v = 0
}
@@ -2975,9 +2975,9 @@ func transformRemoveAbovePercentile(ec *evalConfig, fe *graphiteql.FuncExpr) (ne
}
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
values := s.Values
maxV := aggrFunc(values)
max := aggrFunc(values)
for i, v := range values {
if v > maxV {
if v > max {
values[i] = nan
}
}
@@ -3035,9 +3035,9 @@ func transformRemoveBelowPercentile(ec *evalConfig, fe *graphiteql.FuncExpr) (ne
}
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
values := s.Values
minV := aggrFunc(values)
min := aggrFunc(values)
for i, v := range values {
if v < minV {
if v < min {
values[i] = nan
}
}
@@ -4514,11 +4514,11 @@ func transformOffsetToZero(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesF
}
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
values := s.Values
minV := aggrMin(values)
min := aggrMin(values)
for i, v := range values {
values[i] = v - minV
values[i] = v - min
}
s.Tags["offsetToZero"] = fmt.Sprintf("%g", minV)
s.Tags["offsetToZero"] = fmt.Sprintf("%g", min)
s.Name = fmt.Sprintf("offsetToZero(%s)", s.Name)
s.expr = fe
s.pathExpression = s.Name
@@ -4567,29 +4567,29 @@ func transformPerSecond(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc
return f, nil
}
func nonNegativeDelta(currV, prevV, maxV, minV float64) (float64, float64) {
if !math.IsNaN(maxV) && currV > maxV {
func nonNegativeDelta(curr, prev, max, min float64) (float64, float64) {
if !math.IsNaN(max) && curr > max {
return nan, nan
}
if !math.IsNaN(minV) && currV < minV {
if !math.IsNaN(min) && curr < min {
return nan, nan
}
if math.IsNaN(currV) || math.IsNaN(prevV) {
return nan, currV
if math.IsNaN(curr) || math.IsNaN(prev) {
return nan, curr
}
if currV >= prevV {
return currV - prevV, currV
if curr >= prev {
return curr - prev, curr
}
if !math.IsNaN(maxV) {
if math.IsNaN(minV) {
minV = float64(0)
if !math.IsNaN(max) {
if math.IsNaN(min) {
min = float64(0)
}
return maxV + 1 + currV - prevV - minV, currV
return max + 1 + curr - prev - min, curr
}
if !math.IsNaN(minV) {
return currV - minV, currV
if !math.IsNaN(min) {
return curr - min, curr
}
return nan, currV
return nan, curr
}
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.threshold
@@ -4941,8 +4941,8 @@ func transformSortByMinima(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesF
}
// Filter out series with all the values smaller than 0
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
maxV := aggrMax(s.Values)
if math.IsNaN(maxV) || maxV <= 0 {
max := aggrMax(s.Values)
if math.IsNaN(max) || max <= 0 {
return nil, nil
}
return s, nil

View File

@@ -29,13 +29,13 @@ import (
)
var (
deleteAuthKey = flagutil.NewPassword("deleteAuthKey", "authKey for metrics' deletion via /api/v1/admin/tsdb/delete_series and /tags/delSeries. It could be passed via authKey query arg. It overrides -httpAuth.*")
deleteAuthKey = flagutil.NewPassword("deleteAuthKey", "authKey for metrics' deletion via /api/v1/admin/tsdb/delete_series and /tags/delSeries. It overrides -httpAuth.*")
maxConcurrentRequests = flag.Int("search.maxConcurrentRequests", getDefaultMaxConcurrentRequests(), "The maximum number of concurrent search requests. "+
"It shouldn't be high, since a single request can saturate all the CPU cores, while many concurrently executed requests may require high amounts of memory. "+
"See also -search.maxQueueDuration and -search.maxMemoryPerQuery")
maxQueueDuration = flag.Duration("search.maxQueueDuration", 10*time.Second, "The maximum time the request waits for execution when -search.maxConcurrentRequests "+
"limit is reached; see also -search.maxQueryDuration")
resetCacheAuthKey = flagutil.NewPassword("search.resetCacheAuthKey", "Optional authKey for resetting rollup cache via /internal/resetRollupResultCache call. It could be passed via authKey query arg. It overrides -httpAuth.*")
resetCacheAuthKey = flagutil.NewPassword("search.resetCacheAuthKey", "Optional authKey for resetting rollup cache via /internal/resetRollupResultCache call. It overrides -httpAuth.*")
logSlowQueryDuration = flag.Duration("search.logSlowQueryDuration", 5*time.Second, "Log queries with execution time exceeding this value. Zero disables slow query logging. "+
"See also -search.logQueryMemoryUsage")
vmalertProxyURL = flag.String("vmalert.proxyURL", "", "Optional URL for proxying requests to vmalert. For example, if -vmalert.proxyURL=http://vmalert:8880 , then alerting API requests such as /api/v1/rules from Grafana will be proxied to http://vmalert:8880/api/v1/rules")

View File

@@ -481,8 +481,6 @@ func DeleteHandler(startTime time.Time, r *http.Request) error {
if err != nil {
return err
}
cp.deadline = searchutils.GetDeadlineForDelete(r, startTime)
if !cp.IsDefaultTimeRange() {
return fmt.Errorf("start=%d and end=%d args aren't supported. Remove these args from the query in order to delete all the matching metrics", cp.start, cp.end)
}

View File

@@ -295,13 +295,13 @@ func aggrFuncMin(tss []*timeseries) []*timeseries {
}
dst := tss[0]
for i := range dst.Values {
minV := dst.Values[i]
min := dst.Values[i]
for _, ts := range tss {
if math.IsNaN(minV) || ts.Values[i] < minV {
minV = ts.Values[i]
if math.IsNaN(min) || ts.Values[i] < min {
min = ts.Values[i]
}
}
dst.Values[i] = minV
dst.Values[i] = min
}
return tss[:1]
}
@@ -313,13 +313,13 @@ func aggrFuncMax(tss []*timeseries) []*timeseries {
}
dst := tss[0]
for i := range dst.Values {
maxV := dst.Values[i]
max := dst.Values[i]
for _, ts := range tss {
if math.IsNaN(maxV) || ts.Values[i] > maxV {
maxV = ts.Values[i]
if math.IsNaN(max) || ts.Values[i] > max {
max = ts.Values[i]
}
}
dst.Values[i] = maxV
dst.Values[i] = max
}
return tss[:1]
}
@@ -793,7 +793,7 @@ func fillNaNsAtIdx(idx int, k float64, tss []*timeseries) {
}
}
func getIntK(k float64, maxV int) int {
func getIntK(k float64, max int) int {
if math.IsNaN(k) {
return 0
}
@@ -801,38 +801,38 @@ func getIntK(k float64, maxV int) int {
if kn < 0 {
return 0
}
if kn > maxV {
return maxV
if kn > max {
return max
}
return kn
}
func minValue(values []float64) float64 {
minV := nan
for len(values) > 0 && math.IsNaN(minV) {
minV = values[0]
min := nan
for len(values) > 0 && math.IsNaN(min) {
min = values[0]
values = values[1:]
}
for _, v := range values {
if !math.IsNaN(v) && v < minV {
minV = v
if !math.IsNaN(v) && v < min {
min = v
}
}
return minV
return min
}
func maxValue(values []float64) float64 {
maxV := nan
for len(values) > 0 && math.IsNaN(maxV) {
maxV = values[0]
max := nan
for len(values) > 0 && math.IsNaN(max) {
max = values[0]
values = values[1:]
}
for _, v := range values {
if !math.IsNaN(v) && v > maxV {
maxV = v
if !math.IsNaN(v) && v > max {
max = v
}
}
return maxV
return max
}
func avgValue(values []float64) float64 {

View File

@@ -374,8 +374,8 @@ func getRollupConfigs(funcName string, rf rollupFunc, expr metricsql.Expr, start
preFunc := func(_ []float64, _ []int64) {}
funcName = strings.ToLower(funcName)
if rollupFuncsRemoveCounterResets[funcName] {
preFunc = func(values []float64, timestamps []int64) {
removeCounterResets(values, timestamps, lookbackDelta)
preFunc = func(values []float64, _ []int64) {
removeCounterResets(values)
}
}
samplesScannedPerCall := rollupFuncsSamplesScannedPerCall[funcName]
@@ -486,8 +486,8 @@ func getRollupConfigs(funcName string, rf rollupFunc, expr metricsql.Expr, start
for _, aggrFuncName := range aggrFuncNames {
if rollupFuncsRemoveCounterResets[aggrFuncName] {
// There is no need to save the previous preFunc, since it is either empty or the same.
preFunc = func(values []float64, timestamps []int64) {
removeCounterResets(values, timestamps, lookbackDelta)
preFunc = func(values []float64, _ []int64) {
removeCounterResets(values)
}
}
rf := rollupAggrFuncs[aggrFuncName]
@@ -521,7 +521,7 @@ type rollupFuncArg struct {
timestamps []int64
// Real value preceding values.
// Is populated if preceding value is within the -search.maxStalenessInterval (rc.LookbackDelta).
// Is populated if preceding value is within the staleness interval.
realPrevValue float64
// Real value which goes after values.
@@ -768,13 +768,7 @@ func (rc *rollupConfig) doInternal(dstValues []float64, tsm *timeseriesMap, valu
rfa.realPrevValue = nan
if i > 0 {
prevValue, prevTimestamp := values[i-1], timestamps[i-1]
// set realPrevValue if rc.LookbackDelta == 0
// or if distance between datapoint in prev interval and beginning of this interval
// doesn't exceed LookbackDelta.
// https://github.com/VictoriaMetrics/VictoriaMetrics/pull/1381
// https://github.com/VictoriaMetrics/VictoriaMetrics/issues/894
// https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8045
if rc.LookbackDelta == 0 || (tStart-prevTimestamp) < rc.LookbackDelta {
if (tEnd - prevTimestamp) < maxPrevInterval {
rfa.realPrevValue = prevValue
}
}
@@ -900,7 +894,7 @@ func getMaxPrevInterval(scrapeInterval int64) int64 {
return scrapeInterval + scrapeInterval/8
}
func removeCounterResets(values []float64, timestamps []int64, maxStalenessInterval int64) {
func removeCounterResets(values []float64) {
// There is no need in handling NaNs here, since they are impossible
// on values from vmstorage.
if len(values) == 0 {
@@ -919,16 +913,6 @@ func removeCounterResets(values []float64, timestamps []int64, maxStalenessInter
correction += prevValue
}
}
if i > 0 && maxStalenessInterval > 0 {
gap := timestamps[i] - timestamps[i-1]
if gap > maxStalenessInterval {
// reset correction if gap between samples exceeds staleness interval
// see https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8072
correction = 0
prevValue = v
continue
}
}
prevValue = v
values[i] = v + correction
// Check again, there could be precision error in float operations,
@@ -1701,9 +1685,9 @@ func rollupRateOverSum(rfa *rollupFuncArg) float64 {
}
func rollupRange(rfa *rollupFuncArg) float64 {
maxV := rollupMax(rfa)
minV := rollupMin(rfa)
return maxV - minV
max := rollupMax(rfa)
min := rollupMin(rfa)
return max - min
}
func rollupSum2(rfa *rollupFuncArg) float64 {
@@ -2211,38 +2195,38 @@ func rollupClose(rfa *rollupFuncArg) float64 {
func rollupHigh(rfa *rollupFuncArg) float64 {
values := getCandlestickValues(rfa)
maxV := getFirstValueForCandlestick(rfa)
if math.IsNaN(maxV) {
max := getFirstValueForCandlestick(rfa)
if math.IsNaN(max) {
if len(values) == 0 {
return nan
}
maxV = values[0]
max = values[0]
values = values[1:]
}
for _, v := range values {
if v > maxV {
maxV = v
if v > max {
max = v
}
}
return maxV
return max
}
func rollupLow(rfa *rollupFuncArg) float64 {
values := getCandlestickValues(rfa)
minV := getFirstValueForCandlestick(rfa)
if math.IsNaN(minV) {
min := getFirstValueForCandlestick(rfa)
if math.IsNaN(min) {
if len(values) == 0 {
return nan
}
minV = values[0]
min = values[0]
values = values[1:]
}
for _, v := range values {
if v < minV {
minV = v
if v < min {
min = v
}
}
return minV
return min
}
func rollupModeOverTime(rfa *rollupFuncArg) float64 {

View File

@@ -117,49 +117,31 @@ func TestRollupIderivDuplicateTimestamps(t *testing.T) {
}
func TestRemoveCounterResets(t *testing.T) {
removeCounterResets(nil, nil, 0)
removeCounterResets(nil)
values := append([]float64{}, testValues...)
timestamps := append([]int64{}, testTimestamps...)
removeCounterResets(values, timestamps, 0)
removeCounterResets(values)
valuesExpected := []float64{123, 157, 167, 188, 221, 255, 320, 332, 364, 396, 398, 398}
testRowsEqual(t, values, testTimestamps, valuesExpected, testTimestamps)
// removeCounterResets doesn't expect negative values, so it doesn't work properly with them.
values = []float64{-100, -200, -300, -400}
timestampsExpected := []int64{0, 1, 2, 3}
removeCounterResets(values, timestampsExpected, 0)
removeCounterResets(values)
valuesExpected = []float64{-100, -100, -100, -100}
timestampsExpected := []int64{0, 1, 2, 3}
testRowsEqual(t, values, timestampsExpected, valuesExpected, timestampsExpected)
// verify how partial counter reset is handled.
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2787
values = []float64{100, 95, 120, 119, 139, 50}
timestampsExpected = []int64{0, 1, 2, 3, 4, 5}
removeCounterResets(values, timestampsExpected, 0)
removeCounterResets(values)
valuesExpected = []float64{100, 100, 125, 125, 145, 195}
timestampsExpected = []int64{0, 1, 2, 3, 4, 5}
testRowsEqual(t, values, timestampsExpected, valuesExpected, timestampsExpected)
// verify that staleness interval is respected during resets
// see https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8072
values = []float64{10, 12, 14, 4, 6, 8, 6, 8, 4, 6}
timestamps = []int64{10, 20, 30, 60, 70, 80, 90, 100, 120, 130}
valuesExpected = []float64{10, 12, 14, 4, 6, 8, 14, 16, 4, 6}
removeCounterResets(values, timestamps, 10)
testRowsEqual(t, values, timestamps, valuesExpected, timestamps)
// verify that staleness is respected if there was no counter reset
// but correction was made previously
values = []float64{10, 12, 2, 4}
timestamps = []int64{10, 20, 30, 60}
valuesExpected = []float64{10, 12, 14, 4}
removeCounterResets(values, timestamps, 10)
testRowsEqual(t, values, timestamps, valuesExpected, timestamps)
// verify results always increase monotonically with possible float operations precision error
values = []float64{34.094223, 2.7518, 2.140669, 0.044878, 1.887095, 2.546569, 2.490149, 0.045, 0.035684, 0.062454, 0.058296}
timestampsExpected = []int64{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
removeCounterResets(values, timestampsExpected, 0)
removeCounterResets(values)
var prev float64
for i, v := range values {
if v < prev {
@@ -184,7 +166,7 @@ func TestDeltaValues(t *testing.T) {
// remove counter resets
values = append([]float64{}, testValues...)
removeCounterResets(values, testTimestamps, 0)
removeCounterResets(values)
deltaValues(values)
valuesExpected = []float64{34, 10, 21, 33, 34, 65, 12, 32, 32, 2, 0, 0}
testRowsEqual(t, values, testTimestamps, valuesExpected, testTimestamps)
@@ -206,7 +188,7 @@ func TestDerivValues(t *testing.T) {
// remove counter resets
values = append([]float64{}, testValues...)
removeCounterResets(values, testTimestamps, 0)
removeCounterResets(values)
derivValues(values, testTimestamps)
valuesExpected = []float64{3400, 1111.111111111111, 1750, 2538.4615384615386, 3090.909090909091, 3611.1111111111113,
6000, 1882.3529411764705, 1777.7777777777778, 400, 0, 0}
@@ -237,7 +219,7 @@ func testRollupFunc(t *testing.T, funcName string, args []any, vExpected float64
rfa.timestamps = append(rfa.timestamps, testTimestamps...)
rfa.window = rfa.timestamps[len(rfa.timestamps)-1] - rfa.timestamps[0]
if rollupFuncsRemoveCounterResets[funcName] {
removeCounterResets(rfa.values, rfa.timestamps, 0)
removeCounterResets(rfa.values)
}
for i := 0; i < 5; i++ {
v := rf(&rfa)
@@ -1608,60 +1590,17 @@ func TestRollupDelta(t *testing.T) {
f(100, nan, nan, nil, 0)
}
func TestRollupDeltaWithStaleness(t *testing.T) {
func TestRollupIncreaseWithStaleness(t *testing.T) {
// there is a gap between samples in the dataset below
timestamps := []int64{0, 15000, 30000, 70000}
values := []float64{1, 1, 1, 1}
// if step > gap, then delta will always respect value before gap
t.Run("step>gap", func(t *testing.T) {
t.Run("step > gap", func(t *testing.T) {
rc := rollupConfig{
Func: rollupDelta,
Start: 0,
End: 70000,
Step: 45000,
Window: 0,
MaxPointsPerSeries: 1e4,
}
rc.Timestamps = rc.getTimestamps()
gotValues, samplesScanned := rc.Do(nil, values, timestamps)
if samplesScanned != 7 {
t.Fatalf("expecting 8 samplesScanned from rollupConfig.Do; got %d", samplesScanned)
}
valuesExpected := []float64{1, 0}
timestampsExpected := []int64{0, 45e3}
testRowsEqual(t, gotValues, rc.Timestamps, valuesExpected, timestampsExpected)
})
// even if LookbackDelta < gap
t.Run("step>gap;LookbackDelta<gap", func(t *testing.T) {
rc := rollupConfig{
Func: rollupDelta,
Start: 0,
End: 70000,
Step: 45000,
LookbackDelta: 10e3,
Window: 0,
MaxPointsPerSeries: 1e4,
}
rc.Timestamps = rc.getTimestamps()
gotValues, samplesScanned := rc.Do(nil, values, timestamps)
if samplesScanned != 7 {
t.Fatalf("expecting 8 samplesScanned from rollupConfig.Do; got %d", samplesScanned)
}
valuesExpected := []float64{1, 0}
timestampsExpected := []int64{0, 45e3}
testRowsEqual(t, gotValues, rc.Timestamps, valuesExpected, timestampsExpected)
})
// if step < gap and LookbackDelta==0 then delta will always respect value before gap
// as LookbackDelta=0 ignores staleness
t.Run("step<gap;LookbackDelta=0", func(t *testing.T) {
rc := rollupConfig{
Func: rollupDelta,
Start: 0,
End: 70000,
Step: 10000,
LookbackDelta: 0,
Step: 35000,
Window: 0,
MaxPointsPerSeries: 1e4,
}
@@ -1670,14 +1609,12 @@ func TestRollupDeltaWithStaleness(t *testing.T) {
if samplesScanned != 8 {
t.Fatalf("expecting 8 samplesScanned from rollupConfig.Do; got %d", samplesScanned)
}
valuesExpected := []float64{1, 0, 0, 0, 0, 0, 0, 0}
timestampsExpected := []int64{0, 10e3, 20e3, 30e3, 40e3, 50e3, 60e3, 70e3}
valuesExpected := []float64{1, 0, 0}
timestampsExpected := []int64{0, 35e3, 70e3}
testRowsEqual(t, gotValues, rc.Timestamps, valuesExpected, timestampsExpected)
})
// if step < gap and LookbackDelta>0 then delta will respect value before gap
// only if it is not stale according to LookbackDelta
t.Run("step<gap;LookbackDelta>0", func(t *testing.T) {
t.Run("step < gap", func(t *testing.T) {
rc := rollupConfig{
Func: rollupDelta,
Start: 0,
@@ -1685,7 +1622,6 @@ func TestRollupDeltaWithStaleness(t *testing.T) {
Step: 10000,
Window: 0,
MaxPointsPerSeries: 1e4,
LookbackDelta: 30e3,
}
rc.Timestamps = rc.getTimestamps()
gotValues, samplesScanned := rc.Do(nil, values, timestamps)
@@ -1720,116 +1656,3 @@ func TestRollupDeltaWithStaleness(t *testing.T) {
testRowsEqual(t, gotValues, rc.Timestamps, valuesExpected, timestampsExpected)
})
}
func TestRollupIncreasePureWithStaleness(t *testing.T) {
// there is a gap between samples in the dataset below
timestamps := []int64{0, 15000, 30000, 70000}
values := []float64{1, 1, 1, 1}
// if step > gap, then delta will always respect value before gap
t.Run("step>gap", func(t *testing.T) {
rc := rollupConfig{
Func: rollupIncreasePure,
Start: 0,
End: 70000,
Step: 45000,
Window: 0,
MaxPointsPerSeries: 1e4,
}
rc.Timestamps = rc.getTimestamps()
gotValues, samplesScanned := rc.Do(nil, values, timestamps)
if samplesScanned != 7 {
t.Fatalf("expecting 8 samplesScanned from rollupConfig.Do; got %d", samplesScanned)
}
valuesExpected := []float64{1, 0}
timestampsExpected := []int64{0, 45e3}
testRowsEqual(t, gotValues, rc.Timestamps, valuesExpected, timestampsExpected)
})
// even if LookbackDelta < gap
t.Run("step>gap;LookbackDelta<gap", func(t *testing.T) {
rc := rollupConfig{
Func: rollupIncreasePure,
Start: 0,
End: 70000,
Step: 45000,
LookbackDelta: 10e3,
Window: 0,
MaxPointsPerSeries: 1e4,
}
rc.Timestamps = rc.getTimestamps()
gotValues, samplesScanned := rc.Do(nil, values, timestamps)
if samplesScanned != 7 {
t.Fatalf("expecting 8 samplesScanned from rollupConfig.Do; got %d", samplesScanned)
}
valuesExpected := []float64{1, 0}
timestampsExpected := []int64{0, 45e3}
testRowsEqual(t, gotValues, rc.Timestamps, valuesExpected, timestampsExpected)
})
// if step < gap and LookbackDelta==0 then delta will always respect value before gap
// as LookbackDelta=0 ignores staleness
t.Run("step<gap;LookbackDelta=0", func(t *testing.T) {
rc := rollupConfig{
Func: rollupIncreasePure,
Start: 0,
End: 70000,
Step: 10000,
LookbackDelta: 0,
Window: 0,
MaxPointsPerSeries: 1e4,
}
rc.Timestamps = rc.getTimestamps()
gotValues, samplesScanned := rc.Do(nil, values, timestamps)
if samplesScanned != 8 {
t.Fatalf("expecting 8 samplesScanned from rollupConfig.Do; got %d", samplesScanned)
}
valuesExpected := []float64{1, 0, 0, 0, 0, 0, 0, 0}
timestampsExpected := []int64{0, 10e3, 20e3, 30e3, 40e3, 50e3, 60e3, 70e3}
testRowsEqual(t, gotValues, rc.Timestamps, valuesExpected, timestampsExpected)
})
// if step < gap and LookbackDelta>0 then delta will respect value before gap
// only if it is not stale according to LookbackDelta
t.Run("step<gap;LookbackDelta>0", func(t *testing.T) {
rc := rollupConfig{
Func: rollupIncreasePure,
Start: 0,
End: 70000,
Step: 10000,
Window: 0,
MaxPointsPerSeries: 1e4,
LookbackDelta: 30e3,
}
rc.Timestamps = rc.getTimestamps()
gotValues, samplesScanned := rc.Do(nil, values, timestamps)
if samplesScanned != 8 {
t.Fatalf("expecting 8 samplesScanned from rollupConfig.Do; got %d", samplesScanned)
}
valuesExpected := []float64{1, 0, 0, 0, 0, 0, 0, 1}
timestampsExpected := []int64{0, 10e3, 20e3, 30e3, 40e3, 50e3, 60e3, 70e3}
testRowsEqual(t, gotValues, rc.Timestamps, valuesExpected, timestampsExpected)
})
// there is a staleness marker between samples in the dataset below
timestamps = []int64{0, 10000, 20000, 30000, 40000}
values = []float64{1, 1, 1, decimal.StaleNaN, 1}
t.Run("staleness marker", func(t *testing.T) {
rc := rollupConfig{
Func: rollupIncreasePure,
Start: 0,
End: 40000,
Step: 10000,
Window: 0,
MaxPointsPerSeries: 1e4,
}
rc.Timestamps = rc.getTimestamps()
gotValues, samplesScanned := rc.Do(nil, values, timestamps)
if samplesScanned != 10 {
t.Fatalf("expecting 10 samplesScanned from rollupConfig.Do; got %d", samplesScanned)
}
valuesExpected := []float64{1, 0, 0, nan, 1}
timestampsExpected := []int64{0, 10e3, 20e3, 30e3, 40e3}
testRowsEqual(t, gotValues, rc.Timestamps, valuesExpected, timestampsExpected)
})
}

View File

@@ -15,7 +15,6 @@ import (
var (
maxExportDuration = flag.Duration("search.maxExportDuration", time.Hour*24*30, "The maximum duration for /api/v1/export call")
maxDeleteDuration = flag.Duration("search.maxDeleteDuration", time.Minute*5, "The maximum duration for /api/v1/admin/tsdb/delete_series call")
maxQueryDuration = flag.Duration("search.maxQueryDuration", time.Second*30, "The maximum duration for query execution. It can be overridden to a smaller value on a per-query basis via 'timeout' query arg")
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. "+
@@ -59,12 +58,6 @@ func GetDeadlineForLabelsAPI(r *http.Request, startTime time.Time) Deadline {
return getDeadlineWithMaxDuration(r, startTime, dMax, "-search.maxLabelsAPIDuration")
}
// GetDeadlineForDelete returns deadline for the given request to /api/v1/admin/tsdb/delete_series.
func GetDeadlineForDelete(r *http.Request, startTime time.Time) Deadline {
dMax := maxDeleteDuration.Milliseconds()
return getDeadlineWithMaxDuration(r, startTime, dMax, "-search.maxDeleteDuration")
}
func getDeadlineWithMaxDuration(r *http.Request, startTime time.Time, dMax int64, flagHint string) Deadline {
d, err := httputils.GetDuration(r, "timeout", 0)
if err != nil {

View File

@@ -1,13 +1,13 @@
{
"files": {
"main.css": "./static/css/main.af583aad.css",
"main.js": "./static/js/main.1413b18d.js",
"main.css": "./static/css/main.63479b72.css",
"main.js": "./static/js/main.256ee243.js",
"static/js/685.f772060c.chunk.js": "./static/js/685.f772060c.chunk.js",
"static/media/MetricsQL.md": "./static/media/MetricsQL.a00044c91d9781cf8557.md",
"index.html": "./index.html"
},
"entrypoints": [
"static/css/main.af583aad.css",
"static/js/main.1413b18d.js"
"static/css/main.63479b72.css",
"static/js/main.256ee243.js"
]
}

View File

@@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="./favicon.svg"/><link rel="apple-touch-icon" href="./favicon.svg"/><link rel="mask-icon" href="./favicon.svg" color="#000000"><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=5"/><meta name="theme-color" content="#000000"/><meta name="description" content="Explore and troubleshoot your VictoriaMetrics data"/><link rel="manifest" href="./manifest.json"/><title>vmui</title><script src="./dashboards/index.js" type="module"></script><meta name="twitter:card" content="summary"><meta name="twitter:title" content="UI for VictoriaMetrics"><meta name="twitter:site" content="@https://victoriametrics.com/"><meta name="twitter:description" content="Explore and troubleshoot your VictoriaMetrics data"><meta name="twitter:image" content="./preview.jpg"><meta property="og:type" content="website"><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 defer="defer" src="./static/js/main.1413b18d.js"></script><link href="./static/css/main.af583aad.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="./favicon.svg"/><link rel="apple-touch-icon" href="./favicon.svg"/><link rel="mask-icon" href="./favicon.svg" color="#000000"><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=5"/><meta name="theme-color" content="#000000"/><meta name="description" content="Explore and troubleshoot your VictoriaMetrics data"/><link rel="manifest" href="./manifest.json"/><title>vmui</title><script src="./dashboards/index.js" type="module"></script><meta name="twitter:card" content="summary"><meta name="twitter:title" content="UI for VictoriaMetrics"><meta name="twitter:site" content="@https://victoriametrics.com/"><meta name="twitter:description" content="Explore and troubleshoot your VictoriaMetrics data"><meta name="twitter:image" content="./preview.jpg"><meta property="og:type" content="website"><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 defer="defer" src="./static/js/main.256ee243.js"></script><link href="./static/css/main.63479b72.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,4 @@
FROM golang:1.23.5 AS build-web-stage
FROM golang:1.23.4 AS build-web-stage
COPY build /build
WORKDIR /build
@@ -6,7 +6,7 @@ COPY web/ /build/
RUN GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o web-amd64 github.com/VictoriMetrics/vmui/ && \
GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -o web-windows github.com/VictoriMetrics/vmui/
FROM alpine:3.21.2
FROM alpine:3.21.0
USER root
COPY --from=build-web-stage /build/web-amd64 /app/web

View File

@@ -5,6 +5,7 @@ import "./style.scss";
import useStateSearchParams from "../../../../hooks/useStateSearchParams";
import { useSearchParams } from "react-router-dom";
import Button from "../../../Main/Button/Button";
import classNames from "classnames";
import { SettingsIcon, VisibilityIcon, VisibilityOffIcon } from "../../../Main/Icons";
import Tooltip from "../../../Main/Tooltip/Tooltip";
import Popper from "../../../Main/Popper/Popper";
@@ -23,20 +24,27 @@ const BarHitsOptions: FC<Props> = ({ onChange }) => {
setFalse: handleCloseOptions,
} = useBoolean(false);
const [graphStyle, setGraphStyle] = useStateSearchParams(GRAPH_STYLES.LINE_STEPPED, "graph");
const [stacked, setStacked] = useStateSearchParams(false, "stacked");
const [fill, setFill] = useStateSearchParams("true", "fill");
const [fill, setFill] = useStateSearchParams(false, "fill");
const [hideChart, setHideChart] = useStateSearchParams(false, "hide_chart");
const options: GraphOptions = useMemo(() => ({
graphStyle: GRAPH_STYLES.BAR,
graphStyle,
stacked,
fill: fill === "true",
fill,
hideChart,
}), [stacked, fill, hideChart]);
}), [graphStyle, stacked, fill, hideChart]);
const handleChangeGraphStyle = (val: string) => () => {
setGraphStyle(val as GRAPH_STYLES);
searchParams.set("graph", val);
setSearchParams(searchParams);
};
const handleChangeFill = (val: boolean) => {
setFill(`${val}`);
searchParams.set("fill", `${val}`);
setFill(val);
val ? searchParams.set("fill", "true") : searchParams.delete("fill");
setSearchParams(searchParams);
};
@@ -89,6 +97,21 @@ const BarHitsOptions: FC<Props> = ({ onChange }) => {
title={"Graph settings"}
>
<div className="vm-bar-hits-options-settings">
<div className="vm-bar-hits-options-settings-item vm-bar-hits-options-settings-item_list">
<p className="vm-bar-hits-options-settings-item__title">Graph style:</p>
{Object.values(GRAPH_STYLES).map(style => (
<div
key={style}
className={classNames({
"vm-list-item": true,
"vm-list-item_active": graphStyle === style,
})}
onClick={handleChangeGraphStyle(style)}
>
{style}
</div>
))}
</div>
<div className="vm-bar-hits-options-settings-item">
<Switch
label={"Stacked"}
@@ -99,7 +122,7 @@ const BarHitsOptions: FC<Props> = ({ onChange }) => {
<div className="vm-bar-hits-options-settings-item">
<Switch
label={"Fill"}
value={fill === "true"}
value={fill}
onChange={handleChangeFill}
/>
</div>

View File

@@ -11,12 +11,12 @@
&-settings {
display: grid;
align-items: flex-start;
min-width: 200px;
gap: $padding-global;
padding-bottom: $padding-global;
min-width: 200px;
&-item {
padding: 0 $padding-global;
border-bottom: $border-divider;
padding: 0 $padding-global $padding-global;
&_list {
padding: 0;

View File

@@ -124,7 +124,7 @@ const QueryEditor: FC<QueryEditorProps> = ({
};
useEffect(() => {
setOpenAutocomplete(!!AutocompleteEl && autocompleteQuick);
setOpenAutocomplete(!!AutocompleteEl);
}, [autocompleteQuick]);
useEffect(() => {

View File

@@ -1,246 +0,0 @@
import React, { FC, useMemo, useState } from "preact/compat";
import useBoolean from "../../../hooks/useBoolean";
import { RestartIcon, SettingsIcon } from "../../Main/Icons";
import Button from "../../Main/Button/Button";
import Modal from "../../Main/Modal/Modal";
import Tooltip from "../../Main/Tooltip/Tooltip";
import { Logs } from "../../../api/types";
import Select from "../../Main/Select/Select";
import { useSearchParams } from "react-router-dom";
import "./style.scss";
import Switch from "../../Main/Switch/Switch";
import TextField from "../../Main/TextField/TextField";
import dayjs from "dayjs";
import Hyperlink from "../../Main/Hyperlink/Hyperlink";
import {
LOGS_DISPLAY_FIELDS,
LOGS_GROUP_BY,
LOGS_DATE_FORMAT,
LOGS_URL_PARAMS,
WITHOUT_GROUPING
} from "../../../constants/logs";
const {
GROUP_BY,
NO_WRAP_LINES,
COMPACT_GROUP_HEADER,
DISPLAY_FIELDS,
DATE_FORMAT
} = LOGS_URL_PARAMS;
const title = "Group view settings";
interface Props {
logs: Logs[];
}
const GroupLogsConfigurators: FC<Props> = ({ logs }) => {
const [searchParams, setSearchParams] = useSearchParams();
const groupBy = searchParams.get(GROUP_BY) || LOGS_GROUP_BY;
const noWrapLines = searchParams.get(NO_WRAP_LINES) === "true";
const compactGroupHeader = searchParams.get(COMPACT_GROUP_HEADER) === "true";
const displayFieldsString = searchParams.get(DISPLAY_FIELDS) || "";
const displayFields = displayFieldsString ? displayFieldsString.split(",") : [];
const [dateFormat, setDateFormat] = useState(searchParams.get(DATE_FORMAT) || LOGS_DATE_FORMAT);
const [errorFormat, setErrorFormat] = useState("");
const isGroupChanged = groupBy !== LOGS_GROUP_BY;
const isDisplayFieldsChanged = displayFields.length > 0;
const isTimeChanged = searchParams.get(DATE_FORMAT) !== LOGS_DATE_FORMAT;
const hasChanges = [
isGroupChanged,
isDisplayFieldsChanged,
noWrapLines,
compactGroupHeader,
isTimeChanged
].some(Boolean);
const logsKeys = useMemo(() => {
const excludeKeys = ["_msg", "_time"];
const uniqKeys = Array.from(new Set(logs.map(l => Object.keys(l)).flat()));
return uniqKeys.filter(k => !excludeKeys.includes(k));
}, [logs]);
const {
value: openModal,
toggle: toggleOpen,
setFalse: handleClose,
} = useBoolean(false);
const handleSelectGroupBy = (key: string) => {
searchParams.set(GROUP_BY, key);
setSearchParams(searchParams);
};
const handleSelectDisplayField = (value: string) => {
const prev = displayFields;
const newDisplayFields = prev.includes(value) ? prev.filter(v => v !== value) : [...prev, value];
searchParams.set(DISPLAY_FIELDS, newDisplayFields.join(","));
setSearchParams(searchParams);
};
const handleResetDisplayFields = () => {
searchParams.delete(DISPLAY_FIELDS);
setSearchParams(searchParams);
};
const toggleWrapLines = () => {
searchParams.set(NO_WRAP_LINES, String(!noWrapLines));
setSearchParams(searchParams);
};
const toggleCompactGroupHeader = () => {
searchParams.set(COMPACT_GROUP_HEADER, String(!compactGroupHeader));
setSearchParams(searchParams);
};
const handleChangeDateFormat = (format: string) => {
const date = new Date();
if (!dayjs(date, format, true).isValid()) {
setErrorFormat("Invalid date format");
}
setDateFormat(format);
};
const handleSaveAndClose = () => {
searchParams.set(DATE_FORMAT, dateFormat);
setSearchParams(searchParams);
handleClose();
};
const tooltipContent = () => {
if (!hasChanges) return title;
return (
<div className="vm-group-logs-configurator__tooltip">
<p>{title}</p>
<hr/>
<ul>
{isGroupChanged && <li>Group by <code>{`"${groupBy}"`}</code></li>}
{isDisplayFieldsChanged && <li>Display fields: {displayFields.length || 1}</li>}
{noWrapLines && <li>Single-line text is enabled</li>}
{compactGroupHeader && <li>Compact group header is enabled</li>}
{isTimeChanged && <li>Date format: <code>{dateFormat}</code></li>}
</ul>
</div>
);
};
return (
<>
<div className="vm-group-logs-configurator-button">
<Tooltip title={tooltipContent()}>
<Button
variant="text"
startIcon={<SettingsIcon/>}
onClick={toggleOpen}
ariaLabel={title}
/>
</Tooltip>
{hasChanges && <span className="vm-group-logs-configurator-button__marker"/>}
</div>
{openModal && (
<Modal
title={title}
onClose={handleSaveAndClose}
>
<div className="vm-group-logs-configurator">
<div className="vm-group-logs-configurator-item">
<Select
value={groupBy}
list={[WITHOUT_GROUPING, ...logsKeys]}
label="Group by field"
placeholder="Group by field"
onChange={handleSelectGroupBy}
searchable
/>
<Tooltip title={"Reset grouping"}>
<Button
variant="text"
color="primary"
startIcon={<RestartIcon/>}
onClick={() => handleSelectGroupBy(LOGS_GROUP_BY)}
/>
</Tooltip>
<span className="vm-group-logs-configurator-item__info">
Select a field to group logs by (default: <code>{LOGS_GROUP_BY}</code>).
</span>
</div>
<div className="vm-group-logs-configurator-item">
<Select
value={displayFields}
list={logsKeys}
label="Display fields"
placeholder="Display fields"
onChange={handleSelectDisplayField}
searchable
/>
<Tooltip title={"Clear fields"}>
<Button
variant="text"
color="primary"
startIcon={<RestartIcon/>}
onClick={handleResetDisplayFields}
/>
</Tooltip>
<span className="vm-group-logs-configurator-item__info">
Select fields to display instead of the message (default: <code>{LOGS_DISPLAY_FIELDS}</code>).
</span>
</div>
<div className="vm-group-logs-configurator-item">
<TextField
autofocus
label="Date format"
value={dateFormat}
onChange={handleChangeDateFormat}
error={errorFormat}
/>
<Tooltip title={"Reset format"}>
<Button
variant="text"
color="primary"
startIcon={<RestartIcon/>}
onClick={() => setDateFormat(LOGS_DATE_FORMAT)}
/>
</Tooltip>
<span className="vm-group-logs-configurator-item__info vm-group-logs-configurator-item__info_input">
Set the date format (e.g., <code>YYYY-MM-DD HH:mm:ss</code>).
Learn more in <Hyperlink
href="https://day.js.org/docs/en/display/format"
>this documentation</Hyperlink>. <br/>
Your current date format: <code>{dayjs().format(dateFormat || LOGS_DATE_FORMAT)}</code>
</span>
</div>
<div className="vm-group-logs-configurator-item">
<Switch
value={noWrapLines}
onChange={toggleWrapLines}
label="Single-line message"
/>
<span className="vm-group-logs-configurator-item__info">
Displays message in a single line and truncates it with an ellipsis if it exceeds the available space
</span>
</div>
<div className="vm-group-logs-configurator-item">
<Switch
value={compactGroupHeader}
onChange={toggleCompactGroupHeader}
label="Compact group header"
/>
<span className="vm-group-logs-configurator-item__info">
Shows group headers in one line with a &quot;+N more&quot; badge for extra fields.
</span>
</div>
</div>
</Modal>
)}
</>
);
};
export default GroupLogsConfigurators;

View File

@@ -1,48 +0,0 @@
@use "src/styles/variables" as *;
.vm-group-logs-configurator {
display: grid;
gap: calc($padding-large * 2);
padding: $padding-global 0;
width: 600px;
&-item {
display: grid;
grid-template-columns: 1fr 31px;
align-items: center;
justify-content: stretch;
gap: 0 $padding-small;
&__info {
margin-top: $padding-small;
grid-column: 1/span 2;
font-size: $font-size-small;
color: $color-text-secondary;
line-height: 130%;
&_input {
margin-top: 0;
}
}
}
&-button {
position: relative;
&__marker {
position: absolute;
top: 6px;
left: 6px;
width: 5px;
height: 5px;
border-radius: 50%;
background-color: $color-secondary;
}
}
&__tooltip {
ul {
list-style-position: inside;
}
}
}

View File

@@ -30,10 +30,6 @@ const Accordion: FC<AccordionProps> = ({
onChange && onChange(isOpen);
}, [isOpen]);
useEffect(() => {
setIsOpen(defaultExpanded);
}, [defaultExpanded]);
return (
<>
<header

View File

@@ -67,11 +67,11 @@ const Modal: FC<ModalProps> = ({
})}
onMouseDown={onClose}
>
<div
className="vm-modal-content"
onMouseDown={handleMouseDown}
>
<div className="vm-modal-content-header">
<div className="vm-modal-content">
<div
className="vm-modal-content-header"
onMouseDown={handleMouseDown}
>
{title && (
<div className="vm-modal-content-header__title">
{title}
@@ -91,6 +91,7 @@ const Modal: FC<ModalProps> = ({
{/* tabIndex to fix Ctrl-A */}
<div
className="vm-modal-content-body"
onMouseDown={handleMouseDown}
tabIndex={0}
>
{children}

View File

@@ -33,9 +33,9 @@
align-items: center;
justify-content: center;
background-color: $color-hover-black;
padding: 2px 2px 2px $padding-small;
padding: 2px 2px 2px 6px;
border-radius: $border-radius-small;
font-size: $font-size-small;
font-size: $font-size;
line-height: $font-size;
max-width: 100%;

View File

@@ -11,7 +11,7 @@ import useBoolean from "../../../hooks/useBoolean";
import TextField from "../../Main/TextField/TextField";
import { KeyboardEvent, useState } from "react";
import Modal from "../../Main/Modal/Modal";
import { useSearchParams } from "react-router-dom";
import { getFromStorage, removeFromStorage, saveToStorage } from "../../../utils/storage";
const title = "Table settings";
@@ -30,8 +30,6 @@ const TableSettings: FC<TableSettingsProps> = ({
onChangeColumns,
toggleTableCompact
}) => {
const [searchParams, setSearchParams] = useSearchParams();
const buttonRef = useRef<HTMLDivElement>(null);
const {
@@ -40,6 +38,11 @@ const TableSettings: FC<TableSettingsProps> = ({
setFalse: handleClose,
} = useBoolean(false);
const {
value: saveColumns,
toggle: toggleSaveColumns,
} = useBoolean(Boolean(getFromStorage("TABLE_COLUMNS")));
const [searchColumn, setSearchColumn] = useState("");
const [indexFocusItem, setIndexFocusItem] = useState(-1);
@@ -57,34 +60,15 @@ const TableSettings: FC<TableSettingsProps> = ({
return filteredColumns.every(col => selectedColumns.includes(col));
}, [selectedColumns, filteredColumns]);
const handleChangeDisplayColumns = (displayColumns: string[]) => {
onChangeColumns(displayColumns);
const updatedParams = new URLSearchParams(searchParams.toString());
const isAllCheck = displayColumns.length === columns.length;
if (isAllCheck) {
updatedParams.delete("columns");
} else {
updatedParams.set("columns", displayColumns.map(encodeURIComponent).join(","));
}
setSearchParams(updatedParams);
};
const handleChange = (key: string) => {
const displayColumns = selectedColumns.includes(key)
? selectedColumns.filter(col => col !== key)
: [...selectedColumns, key];
handleChangeDisplayColumns(displayColumns);
onChangeColumns(selectedColumns.includes(key) ? selectedColumns.filter(col => col !== key) : [...selectedColumns, key]);
};
const toggleAllColumns = () => {
if (isAllChecked) {
handleChangeDisplayColumns(selectedColumns.filter(col => !filteredColumns.includes(col)));
onChangeColumns(selectedColumns.filter(col => !filteredColumns.includes(col)));
} else {
handleChangeDisplayColumns(filteredColumns);
onChangeColumns(filteredColumns);
}
};
@@ -111,16 +95,22 @@ const TableSettings: FC<TableSettingsProps> = ({
};
useEffect(() => {
if (arrayEquals(columns, selectedColumns) || searchParams.has("columns")) return;
if (arrayEquals(columns, selectedColumns) || saveColumns) return;
onChangeColumns(columns);
}, [columns]);
useEffect(() => {
const hasColumns = searchParams.has("columns");
if (!hasColumns) return;
const columnsParam = searchParams.get("columns") || "";
const columnsArray = columnsParam.split(",").map(decodeURIComponent).filter(Boolean);
onChangeColumns(columnsArray);
if (!saveColumns) {
removeFromStorage(["TABLE_COLUMNS"]);
} else if (selectedColumns.length) {
saveToStorage("TABLE_COLUMNS", selectedColumns.join(","));
}
}, [saveColumns, selectedColumns]);
useEffect(() => {
const saveColumns = getFromStorage("TABLE_COLUMNS") as string;
if (!saveColumns) return;
onChangeColumns(saveColumns.split(","));
}, []);
return (
@@ -193,6 +183,19 @@ const TableSettings: FC<TableSettingsProps> = ({
</div>
))}
</div>
<div className="vm-table-settings-modal-preserve">
<Checkbox
checked={saveColumns}
onChange={toggleSaveColumns}
label={"Preserve column settings"}
disabled={tableCompact}
color={"primary"}
/>
<p className="vm-table-settings-modal-preserve__info">
This label indicates that when the checkbox is activated,
the current column configurations will not be reset.
</p>
</div>
</div>
</div>
<div className="vm-table-settings-modal-section">

View File

@@ -3,7 +3,6 @@
.vm-table-settings {
&-modal {
.vm-modal-content-body {
min-width: clamp(300px, 600px, 90vw);
padding: 0;
}
@@ -84,5 +83,16 @@
}
}
}
&-preserve {
padding: $padding-global;
&__info {
padding-top: $padding-small;
font-size: $font-size-small;
color: $color-text-secondary;
line-height: 130%;
}
}
}
}

View File

@@ -1,21 +1,2 @@
import { DATE_TIME_FORMAT } from "./date";
export const LOGS_ENTRIES_LIMIT = 50;
export const LOGS_BARS_VIEW = 100;
// "Ungrouped" is a string that is used as a value for the "groupBy" parameter.
export const WITHOUT_GROUPING = "Ungrouped";
// Default values for the logs configurators.
export const LOGS_GROUP_BY = "_stream";
export const LOGS_DISPLAY_FIELDS = "_msg";
export const LOGS_DATE_FORMAT = `${DATE_TIME_FORMAT}.SSS`;
// URL parameters for the logs page.
export const LOGS_URL_PARAMS = {
GROUP_BY: "groupBy",
DISPLAY_FIELDS: "displayFields",
NO_WRAP_LINES: "noWrapLines",
COMPACT_GROUP_HEADER: "compactGroupHeader",
DATE_FORMAT: "dateFormat",
};

View File

@@ -20,7 +20,7 @@ const useClickOutside = <T extends HTMLElement = HTMLElement>(
handler(event); // Call the handler only if the click is outside of the element passed.
}, [ref, handler]);
useEventListener("mouseup", listener);
useEventListener("mousedown", listener);
useEventListener("touchstart", listener);
};

View File

@@ -1,19 +1,24 @@
import React, { FC, useCallback, useEffect, useMemo } from "preact/compat";
import { useState } from "react";
import React, { FC, useCallback, useEffect, useMemo, useRef } from "preact/compat";
import { MouseEvent, useState } from "react";
import "./style.scss";
import { Logs } from "../../../api/types";
import Accordion from "../../../components/Main/Accordion/Accordion";
import { groupByMultipleKeys } from "../../../utils/array";
import Tooltip from "../../../components/Main/Tooltip/Tooltip";
import useCopyToClipboard from "../../../hooks/useCopyToClipboard";
import GroupLogsItem from "./GroupLogsItem";
import { useAppState } from "../../../state/common/StateContext";
import classNames from "classnames";
import Button from "../../../components/Main/Button/Button";
import { CollapseIcon, ExpandIcon } from "../../../components/Main/Icons";
import { CollapseIcon, ExpandIcon, StorageIcon } from "../../../components/Main/Icons";
import Popper from "../../../components/Main/Popper/Popper";
import TextField from "../../../components/Main/TextField/TextField";
import useBoolean from "../../../hooks/useBoolean";
import useStateSearchParams from "../../../hooks/useStateSearchParams";
import { useSearchParams } from "react-router-dom";
import { getStreamPairs } from "../../../utils/logs";
import GroupLogsConfigurators
from "../../../components/LogsConfigurators/GroupLogsConfigurators/GroupLogsConfigurators";
import GroupLogsHeader from "./GroupLogsHeader";
import { LOGS_DISPLAY_FIELDS, LOGS_GROUP_BY, LOGS_URL_PARAMS, WITHOUT_GROUPING } from "../../../constants/logs";
const WITHOUT_GROUPING = "No Grouping";
interface Props {
logs: Logs[];
@@ -21,22 +26,47 @@ interface Props {
}
const GroupLogs: FC<Props> = ({ logs, settingsRef }) => {
const [searchParams] = useSearchParams();
const { isDarkTheme } = useAppState();
const copyToClipboard = useCopyToClipboard();
const [searchParams, setSearchParams] = useSearchParams();
const [expandGroups, setExpandGroups] = useState<boolean[]>([]);
const [groupBy, setGroupBy] = useStateSearchParams("_stream", "groupBy");
const [copied, setCopied] = useState<string | null>(null);
const [searchKey, setSearchKey] = useState("");
const optionsButtonRef = useRef<HTMLDivElement>(null);
const groupBy = searchParams.get(LOGS_URL_PARAMS.GROUP_BY) || LOGS_GROUP_BY;
const displayFieldsString = searchParams.get(LOGS_URL_PARAMS.DISPLAY_FIELDS) || LOGS_DISPLAY_FIELDS;
const displayFields = displayFieldsString.split(",");
const {
value: openOptions,
toggle: toggleOpenOptions,
setFalse: handleCloseOptions,
} = useBoolean(false);
const expandAll = useMemo(() => expandGroups.every(Boolean), [expandGroups]);
const logsKeys = useMemo(() => {
const excludeKeys = ["_msg", "_time"];
const uniqKeys = Array.from(new Set(logs.map(l => Object.keys(l)).flat()));
return [WITHOUT_GROUPING, ...uniqKeys.filter(k => !excludeKeys.includes(k))];
}, [logs]);
const filteredLogsKeys = useMemo(() => {
if (!searchKey) return logsKeys;
try {
const regexp = new RegExp(searchKey, "i");
return logsKeys.filter(item => regexp.test(item))
.sort((a, b) => (a.match(regexp)?.index || 0) - (b.match(regexp)?.index || 0));
} catch (e) {
return [];
}
}, [logsKeys, searchKey]);
const groupData = useMemo(() => {
return groupByMultipleKeys(logs, [groupBy]).map((item) => {
const streamValue = item.values[0]?.[groupBy] || "";
const pairs = getStreamPairs(streamValue);
// values sorting by time
const values = item.values.sort((a, b) => new Date(b._time).getTime() - new Date(a._time).getTime());
const values = item.values.sort((a,b) => new Date(b._time).getTime() - new Date(a._time).getTime());
return {
keys: item.keys,
keysString: item.keys.join(""),
@@ -46,6 +76,23 @@ const GroupLogs: FC<Props> = ({ logs, settingsRef }) => {
}).sort((a, b) => a.keysString.localeCompare(b.keysString)); // groups sorting
}, [logs, groupBy]);
const handleClickByPair = (value: string) => async (e: MouseEvent<HTMLDivElement>) => {
e.stopPropagation();
const isKeyValue = /(.+)?=(".+")/.test(value);
const copyValue = isKeyValue ? `${value.replace(/=/, ": ")}` : `${groupBy}: "${value}"`;
const isCopied = await copyToClipboard(copyValue);
if (isCopied) {
setCopied(value);
}
};
const handleSelectGroupBy = (key: string) => () => {
setGroupBy(key);
searchParams.set("groupBy", key);
setSearchParams(searchParams);
handleCloseOptions();
};
const handleToggleExpandAll = useCallback(() => {
setExpandGroups(new Array(groupData.length).fill(!expandAll));
}, [expandAll, groupData.length]);
@@ -58,6 +105,11 @@ const GroupLogs: FC<Props> = ({ logs, settingsRef }) => {
});
}, []);
useEffect(() => {
if (copied === null) return;
const timeout = setTimeout(() => setCopied(null), 2000);
return () => clearTimeout(timeout);
}, [copied]);
useEffect(() => {
setExpandGroups(new Array(groupData.length).fill(true));
@@ -72,16 +124,38 @@ const GroupLogs: FC<Props> = ({ logs, settingsRef }) => {
key={item.keysString}
>
<Accordion
key={String(expandGroups[i])}
defaultExpanded={expandGroups[i]}
onChange={handleChangeExpand(i)}
title={groupBy !== WITHOUT_GROUPING && <GroupLogsHeader group={item}/>}
title={groupBy !== WITHOUT_GROUPING && (
<div className="vm-group-logs-section-keys">
<span className="vm-group-logs-section-keys__title">Group by <code>{groupBy}</code>:</span>
{item.pairs.map((pair) => (
<Tooltip
title={copied === pair ? "Copied" : "Copy to clipboard"}
key={`${item.keysString}_${pair}`}
placement={"top-center"}
>
<div
className={classNames({
"vm-group-logs-section-keys__pair": true,
"vm-group-logs-section-keys__pair_dark": isDarkTheme
})}
onClick={handleClickByPair(pair)}
>
{pair}
</div>
</Tooltip>
))}
<span className="vm-group-logs-section-keys__count">{item.values.length} entries</span>
</div>
)}
>
<div className="vm-group-logs-section-rows">
{item.values.map((value) => (
<GroupLogsItem
key={`${value._msg}${value._time}`}
log={value}
displayFields={displayFields}
/>
))}
</div>
@@ -101,7 +175,47 @@ const GroupLogs: FC<Props> = ({ logs, settingsRef }) => {
ariaLabel={expandAll ? "Collapse All" : "Expand All"}
/>
</Tooltip>
<GroupLogsConfigurators logs={logs}/>
<Tooltip title={"Group by"}>
<div ref={optionsButtonRef}>
<Button
variant="text"
startIcon={<StorageIcon/>}
onClick={toggleOpenOptions}
ariaLabel={"Group by"}
/>
</div>
</Tooltip>
{
<Popper
open={openOptions}
placement="bottom-right"
onClose={handleCloseOptions}
buttonRef={optionsButtonRef}
>
<div className="vm-list vm-group-logs-header-keys">
<div className="vm-group-logs-header-keys__search">
<TextField
label="Search key"
value={searchKey}
onChange={setSearchKey}
type="search"
/>
</div>
{filteredLogsKeys.map(id => (
<div
className={classNames({
"vm-list-item": true,
"vm-list-item_active": id === groupBy
})}
key={id}
onClick={handleSelectGroupBy(id)}
>
{id}
</div>
))}
</div>
</Popper>
}
</div>
), settingsRef.current)}
</>

View File

@@ -1,10 +1,8 @@
import React, { FC, memo, useCallback, useEffect, useState } from "preact/compat";
import Tooltip from "../../../components/Main/Tooltip/Tooltip";
import Button from "../../../components/Main/Button/Button";
import { CopyIcon, StorageIcon, VisibilityIcon } from "../../../components/Main/Icons";
import { CopyIcon } from "../../../components/Main/Icons";
import useCopyToClipboard from "../../../hooks/useCopyToClipboard";
import { useSearchParams } from "react-router-dom";
import { LOGS_GROUP_BY, LOGS_URL_PARAMS } from "../../../constants/logs";
interface Props {
field: string;
@@ -13,17 +11,8 @@ interface Props {
const GroupLogsFieldRow: FC<Props> = ({ field, value }) => {
const copyToClipboard = useCopyToClipboard();
const [searchParams, setSearchParams] = useSearchParams();
const [copied, setCopied] = useState<boolean>(false);
const groupBy = searchParams.get(LOGS_URL_PARAMS.GROUP_BY) || LOGS_GROUP_BY;
const displayFieldsString = searchParams.get(LOGS_URL_PARAMS.DISPLAY_FIELDS) || "";
const displayFields = displayFieldsString ? displayFieldsString.split(",") : [];
const isSelectedField = displayFields.includes(field);
const isGroupByField = groupBy === field;
const handleCopy = useCallback(async () => {
if (copied) return;
try {
@@ -34,18 +23,6 @@ const GroupLogsFieldRow: FC<Props> = ({ field, value }) => {
}
}, [copied, copyToClipboard]);
const handleSelectDisplayField = () => {
const prev = displayFields;
const newDisplayFields = prev.includes(field) ? prev.filter(v => v !== field) : [...prev, field];
searchParams.set(LOGS_URL_PARAMS.DISPLAY_FIELDS, newDisplayFields.join(","));
setSearchParams(searchParams);
};
const handleSelectGroupBy = () => {
isGroupByField ? searchParams.delete(LOGS_URL_PARAMS.GROUP_BY) : searchParams.set(LOGS_URL_PARAMS.GROUP_BY, field);
setSearchParams(searchParams);
};
useEffect(() => {
if (copied === null) return;
const timeout = setTimeout(() => setCopied(false), 2000);
@@ -58,7 +35,6 @@ const GroupLogsFieldRow: FC<Props> = ({ field, value }) => {
<div className="vm-group-logs-row-fields-item-controls__wrapper">
<Tooltip title={copied ? "Copied" : "Copy to clipboard"}>
<Button
className="vm-group-logs-row-fields-item-controls__button"
variant="text"
color="gray"
size="small"
@@ -67,34 +43,6 @@ const GroupLogsFieldRow: FC<Props> = ({ field, value }) => {
ariaLabel="copy to clipboard"
/>
</Tooltip>
<Tooltip
key={`${field}_${isSelectedField}_${isGroupByField}`}
title={isSelectedField ? "Hide this field" : "Show this field instead of the message"}
>
<Button
className="vm-group-logs-row-fields-item-controls__button"
variant="text"
color={isSelectedField ? "secondary" : "gray"}
size="small"
startIcon={isSelectedField ? <VisibilityIcon/> : <VisibilityIcon/>}
onClick={handleSelectDisplayField}
ariaLabel="copy to clipboard"
/>
</Tooltip>
<Tooltip
key={`${field}_${isSelectedField}_${isGroupByField}`}
title={isGroupByField ? "Ungroup this field" : "Group by this field"}
>
<Button
className="vm-group-logs-row-fields-item-controls__button"
variant="text"
color={isGroupByField ? "secondary" : "gray"}
size="small"
startIcon={<StorageIcon/>}
onClick={handleSelectGroupBy}
ariaLabel="copy to clipboard"
/>
</Tooltip>
</div>
</td>
<td className="vm-group-logs-row-fields-item__key">{field}</td>

View File

@@ -1,127 +0,0 @@
import React, { FC, useCallback, useEffect, useRef } from "preact/compat";
import classNames from "classnames";
import { useSearchParams } from "react-router-dom";
import { MouseEvent, useState } from "react";
import { useAppState } from "../../../state/common/StateContext";
import { Logs } from "../../../api/types";
import useEventListener from "../../../hooks/useEventListener";
import Popper from "../../../components/Main/Popper/Popper";
import useBoolean from "../../../hooks/useBoolean";
import GroupLogsHeaderItem from "./GroupLogsHeaderItem";
import { LOGS_GROUP_BY, LOGS_URL_PARAMS } from "../../../constants/logs";
interface Props {
group: {
keys: string[]
keysString: string
values: Logs[]
pairs: string[]
};
}
const GroupLogsHeader: FC<Props> = ({ group }) => {
const { isDarkTheme } = useAppState();
const [searchParams] = useSearchParams();
const containerRef = useRef<HTMLDivElement>(null);
const moreRef = useRef<HTMLDivElement>(null);
const {
value: openMore,
toggle: handleToggleMore,
setFalse: handleCloseMore,
} = useBoolean(false);
const [hideParisCount, setHideParisCount] = useState<number>(0);
const groupBy = searchParams.get(LOGS_URL_PARAMS.GROUP_BY) || LOGS_GROUP_BY;
const compactGroupHeader = searchParams.get(LOGS_URL_PARAMS.COMPACT_GROUP_HEADER) === "true";
const pairs = group.pairs;
const hideAboveIndex = pairs.length - hideParisCount - 1;
const handleClickMore = (e: MouseEvent<HTMLDivElement>) => {
e.stopPropagation();
handleToggleMore();
};
const calcVisiblePairsCount = useCallback(() => {
if (!compactGroupHeader || !containerRef.current) {
setHideParisCount(0);
return;
}
const container = containerRef.current;
const containerSize = container.getBoundingClientRect();
const selector = ".vm-group-logs-section-keys__pair:not(.vm-group-logs-section-keys__pair_more)";
const children = Array.from(container.querySelectorAll(selector));
let count = 0;
for (const child of children) {
const { right } = (child as HTMLElement).getBoundingClientRect();
if ((right + 220) > containerSize.width) {
count++;
}
}
setHideParisCount(count);
}, [compactGroupHeader, containerRef]);
useEffect(calcVisiblePairsCount, [group.pairs, compactGroupHeader, containerRef]);
useEventListener("resize", calcVisiblePairsCount);
return (
<div
className={classNames({
"vm-group-logs-section-keys": true,
"vm-group-logs-section-keys_compact": compactGroupHeader,
})}
ref={containerRef}
>
<span className="vm-group-logs-section-keys__title">Group by <code>{groupBy}</code>:</span>
{pairs.map((pair, i) => (
<GroupLogsHeaderItem
key={`${group.keysString}_${pair}`}
pair={pair}
isHide={hideParisCount ? i > hideAboveIndex : false}
/>
))}
{hideParisCount > 0 && (
<>
<div
className={classNames({
"vm-group-logs-section-keys__pair": true,
"vm-group-logs-section-keys__pair_more": true,
"vm-group-logs-section-keys__pair_dark": isDarkTheme
})}
ref={moreRef}
onClick={handleClickMore}
>
+{hideParisCount} more
</div>
<Popper
open={openMore}
buttonRef={moreRef}
placement="bottom-left"
onClose={handleCloseMore}
>
<div className="vm-group-logs-section-keys vm-group-logs-section-keys_popper">
{pairs.slice(hideAboveIndex + 1).map((pair) => (
<GroupLogsHeaderItem
key={`${group.keysString}_${pair}`}
pair={pair}
/>
))}
</div>
</Popper>
</>
)}
<span className="vm-group-logs-section-keys__count">{group.values.length} entries</span>
</div>
)
;
};
export default GroupLogsHeader;

View File

@@ -1,59 +0,0 @@
import React, { FC, useEffect } from "preact/compat";
import { useAppState } from "../../../state/common/StateContext";
import Tooltip from "../../../components/Main/Tooltip/Tooltip";
import classNames from "classnames";
import { MouseEvent, useState } from "react";
import useCopyToClipboard from "../../../hooks/useCopyToClipboard";
import { useSearchParams } from "react-router-dom";
import { LOGS_GROUP_BY, LOGS_URL_PARAMS } from "../../../constants/logs";
interface Props {
pair: string;
isHide?: boolean;
}
const GroupLogsHeaderItem: FC<Props> = ({ pair, isHide }) => {
const { isDarkTheme } = useAppState();
const copyToClipboard = useCopyToClipboard();
const [searchParams] = useSearchParams();
const [copied, setCopied] = useState<string | null>(null);
const groupBy = searchParams.get(LOGS_URL_PARAMS.GROUP_BY) || LOGS_GROUP_BY;
const handleClickByPair = (value: string) => async (e: MouseEvent<HTMLDivElement>) => {
e.stopPropagation();
const isKeyValue = /(.+)?=(".+")/.test(value);
const copyValue = isKeyValue ? `${value.replace(/=/, ": ")}` : `${groupBy}: "${value}"`;
const isCopied = await copyToClipboard(copyValue);
if (isCopied) {
setCopied(value);
}
};
useEffect(() => {
if (copied === null) return;
const timeout = setTimeout(() => setCopied(null), 2000);
return () => clearTimeout(timeout);
}, [copied]);
return (
<Tooltip
title={copied === pair ? "Copied" : "Copy to clipboard"}
placement={"top-center"}
>
<div
className={classNames({
"vm-group-logs-section-keys__pair": true,
"vm-group-logs-section-keys__pair_hide": isHide,
"vm-group-logs-section-keys__pair_dark": isDarkTheme
})}
onClick={handleClickByPair(pair)}
>
{pair}
</div>
</Tooltip>
);
};
export default GroupLogsHeaderItem;

View File

@@ -6,34 +6,28 @@ import { ArrowDownIcon } from "../../../components/Main/Icons";
import classNames from "classnames";
import { useLogsState } from "../../../state/logsPanel/LogsStateContext";
import dayjs from "dayjs";
import { DATE_TIME_FORMAT } from "../../../constants/date";
import { useTimeState } from "../../../state/time/TimeStateContext";
import GroupLogsFieldRow from "./GroupLogsFieldRow";
import { marked } from "marked";
import { useSearchParams } from "react-router-dom";
import { LOGS_DATE_FORMAT, LOGS_URL_PARAMS } from "../../../constants/logs";
interface Props {
log: Logs;
displayFields?: string[];
}
const GroupLogsItem: FC<Props> = ({ log, displayFields = ["_msg"] }) => {
const GroupLogsItem: FC<Props> = ({ log }) => {
const {
value: isOpenFields,
toggle: toggleOpenFields,
} = useBoolean(false);
const [searchParams] = useSearchParams();
const { markdownParsing } = useLogsState();
const { timezone } = useTimeState();
const noWrapLines = searchParams.get(LOGS_URL_PARAMS.NO_WRAP_LINES) === "true";
const dateFormat = searchParams.get(LOGS_URL_PARAMS.DATE_FORMAT) || LOGS_DATE_FORMAT;
const formattedTime = useMemo(() => {
if (!log._time) return "";
return dayjs(log._time).tz().format(dateFormat);
}, [log._time, timezone, dateFormat]);
return dayjs(log._time).tz().format(`${DATE_TIME_FORMAT}.SSS`);
}, [log._time, timezone]);
const formattedMarkdown = useMemo(() => {
if (!markdownParsing || !log._msg) return "";
@@ -44,14 +38,6 @@ const GroupLogsItem: FC<Props> = ({ log, displayFields = ["_msg"] }) => {
const hasFields = fields.length > 0;
const displayMessage = useMemo(() => {
if (displayFields.length) {
return displayFields.filter(field => log[field]).map((field, i) => (
<span
className="vm-group-logs-row-content__sub-msg"
key={field + i}
>{log[field]}</span>
));
}
if (log._msg) return log._msg;
if (!hasFields) return;
const dataObject = fields.reduce<{ [key: string]: string }>((obj, [key, value]) => {
@@ -59,7 +45,7 @@ const GroupLogsItem: FC<Props> = ({ log, displayFields = ["_msg"] }) => {
return obj;
}, {});
return JSON.stringify(dataObject);
}, [log, fields, hasFields, displayFields]);
}, [log, fields, hasFields]);
return (
<div className="vm-group-logs-row">
@@ -90,8 +76,7 @@ const GroupLogsItem: FC<Props> = ({ log, displayFields = ["_msg"] }) => {
className={classNames({
"vm-group-logs-row-content__msg": true,
"vm-group-logs-row-content__msg_empty-msg": !log._msg,
"vm-group-logs-row-content__msg_missing": !displayMessage,
"vm-group-logs-row-content__msg_single-line": noWrapLines,
"vm-group-logs-row-content__msg_missing": !displayMessage
})}
dangerouslySetInnerHTML={(markdownParsing && formattedMarkdown) ? { __html: formattedMarkdown } : undefined}
>

View File

@@ -1,7 +1,5 @@
@use "src/styles/variables" as *;
$font-size-logs: var(--font-size-logs, $font-size-small);
.vm-group-logs {
margin-top: calc(-1 * $padding-medium);
@@ -21,44 +19,22 @@ $font-size-logs: var(--font-size-logs, $font-size-small);
}
&-section {
border-bottom: $border-divider;
&-keys {
position: relative;
display: flex;
align-items: center;
flex-wrap: wrap;
gap: $padding-small;
padding: $padding-small 120px $padding-small 0;
font-size: $font-size-logs;
&_compact {
flex-wrap: nowrap;
overflow: hidden;
}
&_popper {
display: flex;
flex-wrap: nowrap;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
padding: $padding-global;
max-height: 400px;
overflow: auto;
}
border-bottom: $border-divider;
padding: $padding-small 0;
&__title {
font-weight: bold;
white-space: nowrap;
code {
font-family: monospace;
&:before {
content: "\"";
}
&:after {
content: "\"";
}
@@ -66,35 +42,19 @@ $font-size-logs: var(--font-size-logs, $font-size-small);
}
&__count {
position: absolute;
top: auto;
right: 0;
flex-grow: 1;
text-align: right;
font-size: $font-size-logs;
font-size: $font-size-small;
color: $color-text-secondary;
padding-right: calc($padding-large * 3);
}
&__pair {
order: 0;
padding: calc($padding-global / 2) $padding-global;
background-color: lighten($color-tropical-blue, 6%);
color: darken($color-dodger-blue, 20%);
border-radius: $border-radius-medium;
transition: background-color 0.3s ease-in, transform 0.1s ease-in, opacity 0.3s ease-in;
white-space: nowrap;
&_hide {
order: 2;
visibility: hidden;
opacity: 0;
pointer-events: none;
}
&_more {
order: 1;
}
&:hover {
background-color: $color-tropical-blue;
@@ -124,19 +84,13 @@ $font-size-logs: var(--font-size-logs, $font-size-small);
&-row {
position: relative;
&:last-child {
margin-bottom: $padding-small;
}
border-bottom: $border-divider;
&-content {
position: relative;
display: grid;
grid-template-columns: auto max-content 1fr;
padding: calc($padding-small / 4) 0;
font-size: $font-size-logs;
font-variant-numeric: tabular-nums;
line-height: 1.3;
grid-template-columns: auto minmax(180px, max-content) 1fr;
padding: $padding-global 0;
cursor: pointer;
transition: background-color 0.2s ease-in;
@@ -162,7 +116,8 @@ $font-size-logs: var(--font-size-logs, $font-size-small);
display: flex;
align-items: flex-start;
justify-content: flex-end;
padding: 0 $padding-global 0 $padding-small;
margin-right: $padding-small;
line-height: 1;
white-space: nowrap;
&_missing {
@@ -175,12 +130,7 @@ $font-size-logs: var(--font-size-logs, $font-size-small);
&__msg {
font-family: $font-family-monospace;
overflow-wrap: anywhere;
&_single-line {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
line-height: 1.1;
&_empty-msg {
overflow: hidden;
@@ -208,7 +158,7 @@ $font-size-logs: var(--font-size-logs, $font-size-small);
border-radius: $border-radius-small;
tab-size: 4;
font-variant-ligatures: none;
margin: calc($padding-small / 4) 0;
margin: calc($padding-small/4) 0;
}
p {
@@ -221,7 +171,7 @@ $font-size-logs: var(--font-size-logs, $font-size-small);
}
code {
font-size: $font-size-logs;
font-size: $font-size-small;
padding: calc($padding-small / 4) calc($padding-small / 2);
}
@@ -244,35 +194,25 @@ $font-size-logs: var(--font-size-logs, $font-size-small);
blockquote {
border-left: 4px solid $color-hover-black;
margin: calc($padding-small / 2) $padding-small;
padding: calc($padding-small / 2) $padding-small;
margin: calc($padding-small/2) $padding-small;
padding: calc($padding-small/2) $padding-small;
}
ul, ol {
list-style-position: inside;
}
/* end styles for markdown */
}
&__sub-msg {
padding-right: $padding-global;
}
}
&-fields {
position: relative;
grid-row: 2;
padding: $padding-small 0;
margin: $padding-small 0 $padding-small calc($padding-global * 2);
margin-bottom: $padding-small;
border: $border-divider;
border-radius: $border-radius-small;
overflow: auto;
height: 300px;
resize: vertical;
font-family: $font-family-monospace;
font-size: $font-size-logs;
font-variant-numeric: tabular-nums;
max-height: 300px;
&-item {
border-radius: $border-radius-small;
@@ -283,26 +223,19 @@ $font-size-logs: var(--font-size-logs, $font-size-small);
}
&-controls {
padding: 0 calc($padding-small / 2);
padding: 0;
&__wrapper {
display: flex;
align-items: center;
justify-content: center;
}
&__button.vm-button_small {
width: 22px;
height: 22px;
min-height: 22px;
}
}
&__key,
&__value {
vertical-align: top;
line-height: $font-size;
padding: calc($padding-small / 2);
padding: calc($padding-small / 2) $padding-global;
}
&__key {

View File

@@ -3,6 +3,7 @@ export type StorageKeys = "AUTOCOMPLETE"
| "QUERY_TRACING"
| "SERIES_LIMITS"
| "TABLE_COMPACT"
| "TABLE_COLUMNS"
| "TIMEZONE"
| "DISABLED_DEFAULT_TIMEZONE"
| "THEME"

View File

@@ -10,7 +10,7 @@ import (
// specific files
// static content
//
//go:embed favicon.svg robots.txt index.html manifest.json asset-manifest.json
//go:embed favicon-32x32.png robots.txt index.html manifest.json asset-manifest.json
//go:embed static
var files embed.FS

View File

@@ -171,49 +171,4 @@ func TestClusterMultiTenantSelect(t *testing.T) {
t.Errorf("unexpected response (-want, +got):\n%s", diff)
}
// Delete series from specific tenant
vmselect.DeleteSeries(t, "foo_bar", apptest.QueryOpts{
Tenant: "5:15",
})
wantSR = apptest.NewPrometheusAPIV1SeriesResponse(t,
`{"data": [
{"__name__":"foo_bar", "vm_account_id":"0", "vm_project_id":"10"},
{"__name__":"foo_bar", "vm_account_id":"1", "vm_project_id":"1"},
{"__name__":"foo_bar", "vm_account_id":"1", "vm_project_id":"15"},
{"__name__":"foo_bar", "vm_account_id":"5", "vm_project_id":"0"}
]
}`)
wantSR.Sort()
gotSR = vmselect.PrometheusAPIV1Series(t, "foo_bar", apptest.QueryOpts{
Tenant: "multitenant",
Start: "2022-05-10T08:03:00.000Z",
})
gotSR.Sort()
if diff := cmp.Diff(wantSR, gotSR, cmpSROpt); diff != "" {
t.Errorf("unexpected response (-want, +got):\n%s", diff)
}
// Delete series for multitenant with tenant filter
vmselect.DeleteSeries(t, `foo_bar{vm_account_id="1"}`, apptest.QueryOpts{
Tenant: "multitenant",
})
wantSR = apptest.NewPrometheusAPIV1SeriesResponse(t,
`{"data": [
{"__name__":"foo_bar", "vm_account_id":"0", "vm_project_id":"10"},
{"__name__":"foo_bar", "vm_account_id":"5", "vm_project_id":"0"}
]
}`)
wantSR.Sort()
gotSR = vmselect.PrometheusAPIV1Series(t, `foo_bar`, apptest.QueryOpts{
Tenant: "multitenant",
Start: "2022-05-10T08:03:00.000Z",
})
gotSR.Sort()
if diff := cmp.Diff(wantSR, gotSR, cmpSROpt); diff != "" {
t.Errorf("unexpected response (-want, +got):\n%s", diff)
}
}

View File

@@ -117,22 +117,6 @@ func (app *Vmselect) PrometheusAPIV1Series(t *testing.T, matchQuery string, opts
return NewPrometheusAPIV1SeriesResponse(t, res)
}
// DeleteSeries sends a query to a /prometheus/api/v1/admin/tsdb/delete_series
//
// See https://docs.victoriametrics.com/url-examples/#apiv1admintsdbdelete_series
func (app *Vmselect) DeleteSeries(t *testing.T, matchQuery string, opts QueryOpts) {
t.Helper()
seriesURL := fmt.Sprintf("http://%s/delete/%s/prometheus/api/v1/admin/tsdb/delete_series", app.httpListenAddr, opts.getTenant())
values := opts.asURLValues()
values.Add("match[]", matchQuery)
res := app.cli.PostForm(t, seriesURL, values, http.StatusNoContent)
if res != "" {
t.Fatalf("unexpected non-empty DeleteSeries response=%q", res)
}
}
// String returns the string representation of the vmselect app state.
func (app *Vmselect) String() string {
return fmt.Sprintf("{app: %s httpListenAddr: %q}", app.app, app.httpListenAddr)

View File

@@ -1,7 +1,7 @@
dashboard-copy:
echo "" > dashboards/vm/${SRC}
cat dashboards/${SRC} >> dashboards/vm/${SRC}
sed -i='.tmp' 's/prometheus/victoriametrics-metrics-datasource/g' dashboards/vm/${SRC}
sed -i='.tmp' 's/prometheus/victoriametrics-datasource/g' dashboards/vm/${SRC}
sed -i='.tmp' 's/Prometheus/VictoriaMetrics/g' dashboards/vm/${SRC}
sed -i='.tmp' 's/${D_UID}/${D_UID}_vm/g' dashboards/vm/${SRC}
sed -i='.tmp' 's/"title": "${TITLE}"/"title": "${TITLE} (VM)"/g' dashboards/vm/${SRC}

View File

@@ -17,7 +17,7 @@
},
{
"type": "datasource",
"id": "victoriametrics-metrics-datasource",
"id": "victoriametrics-datasource",
"name": "VictoriaMetrics",
"version": "1.0.0"
},
@@ -85,7 +85,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"fieldConfig": {
@@ -133,7 +133,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -150,7 +150,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Status of last backup operation.",
@@ -210,7 +210,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -225,7 +225,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Number of backups stored in remote storage.",
@@ -275,7 +275,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -290,7 +290,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Space used in remote storage.",
@@ -341,7 +341,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -356,7 +356,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "",
@@ -420,7 +420,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -435,7 +435,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Status of last retention run.\n\nRetention is a process of removing old backups from remote storage.",
@@ -495,7 +495,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -510,7 +510,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"fieldConfig": {
@@ -573,7 +573,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -588,7 +588,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"fieldConfig": {
@@ -674,7 +674,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -715,7 +715,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"fieldConfig": {
@@ -801,7 +801,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -852,7 +852,7 @@
"panels": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Max duration of backup run. Lower better.\n\nEach backup starts with data upload during `latest` backup. Subsequent backups (`hourly`, `daily`, `weekly`, `monthly`) are copying date by using server-side copy. ",
@@ -931,7 +931,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -946,7 +946,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"fieldConfig": {
@@ -1020,7 +1020,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -1035,7 +1035,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"fieldConfig": {
@@ -1110,7 +1110,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -1125,7 +1125,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"fieldConfig": {
@@ -1184,7 +1184,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -1227,7 +1227,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"fieldConfig": {
@@ -1298,7 +1298,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -1313,7 +1313,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"fieldConfig": {
@@ -1387,7 +1387,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -1416,7 +1416,7 @@
"panels": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Max duration of retention run. Lower better.\n\nRetention is a process of removing old backups from remote storage.",
@@ -1485,7 +1485,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -1500,7 +1500,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"fieldConfig": {
@@ -1574,7 +1574,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -1603,7 +1603,7 @@
"panels": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Duration of backup run. Lower better.\n\nEach backup starts with data upload during `latest` backup. Subsequent backups (`hourly`, `daily`, `weekly`, `monthly`) are copying date by using server-side copy.\n",
@@ -1671,7 +1671,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -1705,7 +1705,7 @@
"multi": false,
"name": "ds",
"options": [],
"query": "victoriametrics-metrics-datasource",
"query": "victoriametrics-datasource",
"queryValue": "",
"refresh": 1,
"regex": "",
@@ -1764,7 +1764,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"filters": [],

View File

@@ -17,7 +17,7 @@
},
{
"type": "datasource",
"id": "victoriametrics-metrics-datasource",
"id": "victoriametrics-datasource",
"name": "VictoriaMetrics",
"version": "1.0.0"
},
@@ -80,7 +80,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "How many datapoints are inserted into storage per second by accountID and projectID",
@@ -168,7 +168,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -184,7 +184,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Request rate accepted by vmselect nodes per tenant",
@@ -272,7 +272,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -290,7 +290,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Shows the number of active time series with new data points inserted during the last hour. High value may result in ingestion slowdown. \n\nSee following link for details:",
@@ -385,7 +385,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -403,7 +403,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Time spent on query execution per tenant per second",
@@ -491,7 +491,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -509,7 +509,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Shows the amount of on-disk space occupied by data points only. The disk space is used for storing by datapoint and indexdb. There is no option to expose per tenant statistic for indexdb. Usually, indexed takes much less space compared to datapoints. But with a high churn rate, the size of the indexdb could grow significantly.",
@@ -596,7 +596,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -614,7 +614,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Number of new series created over last 24h.",
@@ -702,7 +702,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -731,7 +731,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"fieldConfig": {
@@ -785,7 +785,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -800,7 +800,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"fieldConfig": {
@@ -853,7 +853,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -868,7 +868,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"fieldConfig": {
@@ -921,7 +921,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -936,7 +936,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"fieldConfig": {
@@ -991,7 +991,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -1006,7 +1006,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"fieldConfig": {
@@ -1059,7 +1059,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -1074,7 +1074,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"fieldConfig": {
@@ -1127,7 +1127,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -1142,7 +1142,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"fieldConfig": {
@@ -1242,7 +1242,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -1260,7 +1260,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"fieldConfig": {
@@ -1360,7 +1360,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -1378,7 +1378,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"fieldConfig": {
@@ -1478,7 +1478,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -1496,7 +1496,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"fieldConfig": {
@@ -1596,7 +1596,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -1614,7 +1614,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"fieldConfig": {
@@ -1714,7 +1714,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -1732,7 +1732,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"fieldConfig": {
@@ -1832,7 +1832,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -1868,7 +1868,7 @@
"multi": false,
"name": "ds",
"options": [],
"query": "victoriametrics-metrics-datasource",
"query": "victoriametrics-datasource",
"queryValue": "",
"refresh": 1,
"regex": "",
@@ -1879,7 +1879,7 @@
"allValue": ".*",
"current": {},
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"definition": "label_values(vm_tenant_active_timeseries, accountID)",
@@ -1905,7 +1905,7 @@
"allValue": ".*",
"current": {},
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"definition": "label_values(vm_tenant_active_timeseries{accountID=~\"$accountID\"},projectID)",
@@ -1929,7 +1929,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "PE8D8DB4BEE4E4B22"
},
"filters": [],

View File

@@ -33,7 +33,7 @@
{
"collapsed": false,
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"gridPos": {
@@ -47,7 +47,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"refId": "A"
@@ -58,7 +58,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"gridPos": {
@@ -81,7 +81,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"refId": "A"
@@ -92,7 +92,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Number of objects at kubernetes cluster per each controller",
@@ -147,7 +147,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -162,7 +162,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"fieldConfig": {
@@ -215,7 +215,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -234,7 +234,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": " Shows per namespace watchers for VictoriaMetrics Operator objects (ServiceMonitors, PodMonitors, etc) ",
@@ -288,11 +288,11 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
"expr": "sum(operator_victoriametrics-metrics-datasource_converter_active_watchers)",
"expr": "sum(operator_victoriametrics-datasource_converter_active_watchers)",
"instant": false,
"legendFormat": "__auto",
"range": true,
@@ -304,7 +304,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": " Number of operator instances with obtained leader status. \n Value above 1 indicates that instances with the same job may behave incorrectly.\n It's recommend to check Operator logs. ",
@@ -358,7 +358,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -374,7 +374,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": " Shows number of active reconcile workers",
@@ -428,7 +428,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -444,7 +444,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": " Shows amount of VictoriaMetrics Operator objects processed by Operator.",
@@ -527,11 +527,11 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
"expr": "sum(rate(operator_victoriametrics-metrics-datasource_converter_watch_events_total{job=~\"$job\"}[$__interval])) by (event_type,object_type_name)",
"expr": "sum(rate(operator_victoriametrics-datasource_converter_watch_events_total{job=~\"$job\"}[$__interval])) by (event_type,object_type_name)",
"instant": false,
"legendFormat": "{{object_type_name}} {{event_type}}",
"range": true,
@@ -543,7 +543,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"fieldConfig": {
@@ -627,7 +627,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -642,7 +642,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Shows the rate of logging the messages by their level. Unexpected spike in rate is a good reason to check logs.",
@@ -727,7 +727,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -743,7 +743,7 @@
{
"collapsed": false,
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"gridPos": {
@@ -757,7 +757,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"refId": "A"
@@ -768,7 +768,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Non zero metrics indicates about error with CR object definition (typos or incorrect values) or errors with kubernetes API connection.",
@@ -852,7 +852,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -865,7 +865,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -881,7 +881,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Operator limits number of reconcile configuration events to 5 events per 2 seconds by default.",
@@ -965,7 +965,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -980,7 +980,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Number of objects waiting in the queue for reconciliation. Non-zero values indicate that operator cannot process CR objects changes with the given resources.",
@@ -1064,7 +1064,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -1079,7 +1079,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": " For controllers with StatefulSet it's ok to see latency greater then 3 seconds. It could be vmalertmanager,vmcluster or vmagent in statefulMode.\n\n For other controllers, latency greater then 2 second may indicate issues with kubernetes cluster or operator's performance.\n ",
@@ -1165,7 +1165,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -1180,7 +1180,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Number of HTTP requests to the Kubernetes API server break down by code and method",
@@ -1262,7 +1262,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -1278,7 +1278,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Shows how many ongoing reconcile events are taking place, where:\n* `max` - equal to the value of flag`-controller.maxConcurrentReconciles`;\n* `current` - current number of reconcile workers processing CRD objects.\n\nWhen `current` hits `max` constantly, it means operator cannot process events in time. It should be either increased value for flag `-controller.maxConcurrentReconciles` or allocated additional CPU resources to the operator.",
@@ -1377,7 +1377,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -1389,7 +1389,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -1406,7 +1406,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Shows the time goroutines have spent in runnable state before actually running. The lower is better.\n\nHigh values or values exceeding the threshold is usually a sign of insufficient CPU resources or CPU throttling. \n\nVerify that service has enough CPU resources. Otherwise, the service could work unreliably with delays in processing.",
@@ -1486,7 +1486,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -1502,7 +1502,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": " Requests latency to the Kubernetes API server.",
@@ -1585,7 +1585,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -1602,7 +1602,7 @@
{
"collapsed": true,
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"gridPos": {
@@ -1615,7 +1615,7 @@
"panels": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"fieldConfig": {
@@ -1699,7 +1699,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -1710,7 +1710,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -1722,7 +1722,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -1734,7 +1734,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -1750,7 +1750,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"fieldConfig": {
@@ -1830,7 +1830,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -1845,7 +1845,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"fieldConfig": {
@@ -1925,7 +1925,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -1940,7 +1940,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"fieldConfig": {
@@ -2023,7 +2023,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -2040,7 +2040,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"refId": "A"
@@ -2065,7 +2065,7 @@
"multi": false,
"name": "ds",
"options": [],
"query": "victoriametrics-metrics-datasource",
"query": "victoriametrics-datasource",
"queryValue": "te",
"refresh": 1,
"regex": "",
@@ -2075,7 +2075,7 @@
{
"current": {},
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"definition": "label_values(operator_log_messages_total,job)",
@@ -2097,7 +2097,7 @@
{
"current": {},
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"definition": "label_values(operator_log_messages_total{job=~\"$job\"},instance)",
@@ -2119,7 +2119,7 @@
{
"current": {},
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"definition": "label_values(vm_app_version{job=\"$job\", instance=\"$instance\"}, version)",

View File

@@ -11,7 +11,7 @@
},
{
"type": "datasource",
"id": "victoriametrics-metrics-datasource",
"id": "victoriametrics-datasource",
"name": "VictoriaMetrics",
"version": "1.0.0"
},
@@ -50,7 +50,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"enable": true,
@@ -63,7 +63,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"enable": true,
@@ -97,7 +97,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "How many log entries are in storage",
@@ -149,7 +149,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -168,7 +168,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Shows the logs ingestion rate.",
@@ -220,7 +220,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -239,7 +239,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "The ratio of original data size and compressed data stored on disk",
@@ -291,7 +291,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -310,7 +310,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Total number of available CPUs for VM process",
@@ -366,7 +366,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -385,7 +385,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"fieldConfig": {
@@ -439,7 +439,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -456,7 +456,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Total amount of used disk space",
@@ -508,7 +508,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -527,7 +527,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Shows the rate of HTTP read requests.",
@@ -579,7 +579,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -598,7 +598,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Total size of available memory for VM process",
@@ -680,7 +680,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "How many logs are inserted into storage per second",
@@ -769,7 +769,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -787,7 +787,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "* `*` - unsupported query path\n* `/insert` - insert into VM\n* `/metrics` - query VL system metrics\n* `/query` - read the data",
@@ -877,7 +877,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -895,7 +895,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Shows the amount of on-disk space occupied by data before and after compressiom",
@@ -985,7 +985,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -1003,7 +1003,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "The number of the new log streams created over the last 24h",
@@ -1093,7 +1093,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -1111,7 +1111,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Shows the number of restarts per job. The chart can be useful to identify periodic process restarts and correlate them with potential issues or anomalies. Normally, processes shouldn't restart unless restart was inited by user. The reason of restarts should be figured out by checking the logs of each specific service. ",
@@ -1200,7 +1200,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -1226,7 +1226,7 @@
"panels": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Percentage of used memory (resident).\nThe application's performance will significantly degrade when memory usage is close to 100%.",
@@ -1315,7 +1315,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -1332,7 +1332,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "",
@@ -1421,7 +1421,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"expr": "sum(go_memstats_sys_bytes{job=~\"$job\", instance=~\"$instance\"}) + sum(vm_cache_size_bytes{job=~\"$job\", instance=~\"$instance\"})",
@@ -1433,7 +1433,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"expr": "sum(go_memstats_heap_inuse_bytes{job=~\"$job\", instance=~\"$instance\"}) + sum(vm_cache_size_bytes{job=~\"$job\", instance=~\"$instance\"})",
@@ -1445,7 +1445,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"expr": "sum(go_memstats_stack_inuse_bytes{job=~\"$job\", instance=~\"$instance\"})",
@@ -1457,7 +1457,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"expr": "sum(process_resident_memory_bytes{job=~\"$job\", instance=~\"$instance\"})",
@@ -1470,7 +1470,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"exemplar": false,
@@ -1488,7 +1488,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Share for memory allocated by the process itself. When memory usage reaches 100% it will be likely OOM-killed.\nSafe memory usage % considered to be below 80%",
@@ -1577,7 +1577,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -1594,7 +1594,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"fieldConfig": {
@@ -1682,7 +1682,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -1701,7 +1701,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Shows the percentage of open file descriptors compared to the limit set in the OS.\nReaching the limit of open files can cause various issues and must be prevented.\n\nSee how to change limits here https://medium.com/@muhammadtriwibowo/set-permanently-ulimit-n-open-files-in-ubuntu-4d61064429a",
@@ -1807,7 +1807,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -1826,7 +1826,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "",
@@ -1931,7 +1931,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -1945,7 +1945,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"exemplar": false,
@@ -1963,7 +1963,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"fieldConfig": {
@@ -2052,7 +2052,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -2069,7 +2069,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Shows the number of bytes read/write from the storage layer.",
@@ -2170,7 +2170,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"expr": "sum(rate(process_io_storage_read_bytes_total{job=~\"$job\", instance=~\"$instance\"}[$__rate_interval]))",
@@ -2183,7 +2183,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"expr": "sum(rate(process_io_storage_written_bytes_total{job=~\"$job\", instance=~\"$instance\"}[$__rate_interval]))",
@@ -2200,7 +2200,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"fieldConfig": {
@@ -2289,7 +2289,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -2306,7 +2306,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Shows the number of read/write syscalls such as read, pread, write, pwrite.",
@@ -2407,7 +2407,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -2422,7 +2422,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -2441,7 +2441,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "",
@@ -2530,7 +2530,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -2548,7 +2548,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "",
@@ -2637,7 +2637,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -2655,7 +2655,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Shows the time goroutines have spent in runnable state before actually running. The lower is better.\n\nHigh values or values exceeding the threshold is usually a sign of insufficient CPU resources or CPU throttling. \n\nVerify that service has enough CPU resources. Otherwise, the service could work unreliably with delays in processing.",
@@ -2743,7 +2743,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -2780,7 +2780,7 @@
"multi": false,
"name": "ds",
"options": [],
"query": "victoriametrics-metrics-datasource",
"query": "victoriametrics-datasource",
"queryValue": "",
"refresh": 1,
"regex": "",
@@ -2790,7 +2790,7 @@
{
"current": {},
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"definition": "label_values(vm_app_version{version=~\"victoria-logs-.*\"}, job)",
@@ -2812,7 +2812,7 @@
{
"current": {},
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"definition": "label_values(vm_app_version{job=~\"$job\"}, instance)",
@@ -2833,7 +2833,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"filters": [],

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -11,7 +11,7 @@
},
{
"type": "datasource",
"id": "victoriametrics-metrics-datasource",
"id": "victoriametrics-datasource",
"name": "VictoriaMetrics",
"version": "1.0.0"
},
@@ -56,7 +56,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"enable": true,
@@ -69,7 +69,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"enable": true,
@@ -140,7 +140,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Shows if the last configuration update was successful. \"Not Ok\" means there was an unsuccessful attempt to update the configuration due to some error. Check the log for details.",
@@ -210,7 +210,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"exemplar": false,
@@ -225,7 +225,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Shows the total number of loaded alerting rules across selected instances and groups.",
@@ -273,7 +273,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"exemplar": false,
@@ -288,7 +288,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Shows the total number of loaded recording rules across selected instances and groups.",
@@ -336,7 +336,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"exemplar": false,
@@ -351,7 +351,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Shows the total number of errors generated by recording/alerting rules for selected instances and groups.",
@@ -403,7 +403,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"exemplar": false,
@@ -418,7 +418,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Shows number of Recording Rules which produce no data.\n\n Usually it means that such rules are misconfigured, since they give no output during the evaluation.\nPlease check if rule's expression is correct and it is working as expected.",
@@ -470,7 +470,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -487,7 +487,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"fieldConfig": {
@@ -568,7 +568,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -584,7 +584,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"fieldConfig": {
@@ -672,7 +672,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -716,7 +716,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Shows the number of fired alerts by job.",
@@ -804,7 +804,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -821,7 +821,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Top $topk groups by evaluation duration. Shows groups that take the most of time during the evaluation across all instances.\n\nThe panel uses MetricsQL functions and may not work with VictoriaMetrics.",
@@ -909,7 +909,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -926,7 +926,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Shows how many requests (executions) per second vmalert sends to the configured datasource.",
@@ -1011,7 +1011,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -1028,7 +1028,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Shows the error rate while executing configured rules. Non-zero value means there are some issues with existing rules. Check the logs to get more details.",
@@ -1113,7 +1113,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -1144,7 +1144,7 @@
"panels": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "The precentage of used RSS memory\n\nIf you think that usage is abnormal or unexpected, please file an issue and attach memory profile if possible.",
@@ -1241,7 +1241,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -1258,7 +1258,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Amount of used RSS memory\n\nIf you think that usage is abnormal or unexpected, please file an issue and attach memory profile if possible.",
@@ -1354,7 +1354,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -1371,7 +1371,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Shows the CPU usage percentage per vmalert instance. \nIf you think that usage is abnormal or unexpected pls file an issue and attach CPU profile if possible.",
@@ -1468,7 +1468,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -1487,7 +1487,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Shows the max number of CPU cores used by a `job` and the corresponding limit.",
@@ -1584,7 +1584,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -1599,7 +1599,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -1619,7 +1619,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Panel shows the percentage of open file descriptors in the OS.\nReaching the limit of open files can cause various issues and must be prevented.\n\nSee how to change limits here https://medium.com/@muhammadtriwibowo/set-permanently-ulimit-n-open-files-in-ubuntu-4d61064429a",
@@ -1709,7 +1709,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -1728,7 +1728,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"fieldConfig": {
@@ -1817,7 +1817,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -1835,7 +1835,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Shows the percent of CPU spent on garbage collection.\n\nIf % is high, then CPU usage can be decreased by changing GOGC to higher values. Increasing GOGC value will increase memory usage, and decrease CPU usage.\n\nTry searching for keyword `GOGC` at https://docs.victoriametrics.com/troubleshooting/ ",
@@ -1925,7 +1925,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -1943,7 +1943,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Shows the time goroutines have spent in runnable state before actually running. The lower is better.\n\nHigh values or values exceeding the threshold is usually a sign of insufficient CPU resources or CPU throttling. \n\nVerify that service has enough CPU resources. Otherwise, the service could work unreliably with delays in processing.",
@@ -2031,7 +2031,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -2072,7 +2072,7 @@
"panels": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"fieldConfig": {
@@ -2157,7 +2157,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -2202,7 +2202,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Missed evaluation means that group evaluation time takes longer than the configured evaluation interval. \nThis may result in missed alerting notifications or recording rules samples. Try increasing evaluation interval or concurrency for such groups. See https://docs.victoriametrics.com/vmalert/#groups\n\nIf rule expressions are taking longer than expected, please see https://docs.victoriametrics.com/troubleshooting/#slow-queries.\"",
@@ -2287,7 +2287,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -2304,7 +2304,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Shows the number of restarts per job. The chart can be useful to identify periodic process restarts and correlate them with potential issues or anomalies. Normally, processes shouldn't restart unless restart was inited by user. The reason of restarts should be figured out by checking the logs of each specific service. ",
@@ -2392,7 +2392,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -2426,7 +2426,7 @@
"panels": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Shows top $topk current active (firing) alerting rules.\n\nThe panel uses MetricsQL functions and may not work with VictoriaMetrics.",
@@ -2511,7 +2511,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -2528,7 +2528,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Shows the events when rule execution resulted into an error. Check the logs for more details.",
@@ -2613,7 +2613,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -2630,7 +2630,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Shows the current pending alerting rules per group.\nBy pending means the rule which remains active less than configured `for` parameter.",
@@ -2715,7 +2715,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -2732,7 +2732,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Shows the error rate for the attempts to send alerts to Alertmanager. If not zero it means there issues on attempt to send notification to Alertmanager and some alerts may be not delivered properly. Check the logs for more details.",
@@ -2816,7 +2816,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"exemplar": false,
@@ -2831,7 +2831,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Shows how many alerts are sent to Alertmanager per second. Only active alerts are sent.",
@@ -2915,7 +2915,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -2959,7 +2959,7 @@
"panels": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Shows the top $topk recording rules which generate the most of [samples](https://docs.victoriametrics.com/keyconcepts/#raw-samples). Each generated sample is basically a time series which then ingested into configured remote storage. Rules with high numbers may cause the most pressure on the remote database and become a source of too high cardinality.\n\nThe panel uses MetricsQL functions and may not work with VictoriaMetrics.",
@@ -3044,7 +3044,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -3061,7 +3061,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Shows the rules which do not produce any [samples](https://docs.victoriametrics.com/keyconcepts/#raw-samples) during the evaluation. Usually it means that such rules are misconfigured, since they give no output during the evaluation.\nPlease check if rule's expression is correct and it is working as expected.",
@@ -3146,7 +3146,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -3163,7 +3163,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"fieldConfig": {
@@ -3245,7 +3245,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -3285,7 +3285,7 @@
"panels": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"fieldConfig": {
@@ -3361,7 +3361,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -3376,7 +3376,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Shows the number of datapoints dropped by vmalert while sending to the configured remote write URL. vmalert performs up to 5 retries before dropping the data. Check vmalert's error logs for the specific error message.",
@@ -3453,7 +3453,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -3468,7 +3468,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Shows current number of established connections to remote write endpoints.\n\n",
@@ -3554,7 +3554,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -3571,7 +3571,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Shows the global rate for number of written bytes via remote write connections.",
@@ -3657,7 +3657,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -3696,7 +3696,7 @@
"multi": false,
"name": "ds",
"options": [],
"query": "victoriametrics-metrics-datasource",
"query": "victoriametrics-datasource",
"queryValue": "",
"refresh": 1,
"regex": "",
@@ -3706,7 +3706,7 @@
{
"current": {},
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"definition": "label_values(vm_app_version{version=~\"^vmalert.*\"}, job)",
@@ -3729,7 +3729,7 @@
"allValue": ".*",
"current": {},
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"definition": "label_values(vm_app_version{job=~\"$job\"}, instance)",
@@ -3752,7 +3752,7 @@
"allValue": ".*",
"current": {},
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"definition": "label_values(vmalert_iteration_total{job=~\"$job\", instance=~\"$instance\"},file)",
@@ -3775,7 +3775,7 @@
"allValue": ".*",
"current": {},
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"definition": "label_values(vmalert_iteration_total{job=~\"$job\", instance=~\"$instance\"}, group)",
@@ -3843,7 +3843,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"filters": [],

View File

@@ -11,7 +11,7 @@
},
{
"type": "datasource",
"id": "victoriametrics-metrics-datasource",
"id": "victoriametrics-datasource",
"name": "VictoriaMetrics",
"version": "1.0.0"
},
@@ -116,7 +116,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"fieldConfig": {
@@ -204,7 +204,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -222,7 +222,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Shows if the last configuration update was successful. \"Not Ok\" means there was an unsuccessful attempt to update the configuration due to some error. Check the log for details.",
@@ -292,7 +292,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"exemplar": false,
@@ -307,7 +307,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Shows the rate of requests.",
@@ -356,7 +356,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -372,7 +372,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Shows the total number of users defined at configuration file.",
@@ -420,7 +420,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -437,7 +437,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Shows the rate of request errors.",
@@ -486,7 +486,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -502,7 +502,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"fieldConfig": {
@@ -581,7 +581,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -611,7 +611,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"fieldConfig": {
@@ -690,7 +690,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -702,7 +702,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -718,7 +718,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Shows percent utilization of per concurrent requests capacity.",
@@ -805,7 +805,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -822,7 +822,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Shows the rate of rejected requests by a reason.",
@@ -902,7 +902,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -918,7 +918,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": " The number of concurrent connections processed by vmauth reached one of limits. Possible solutions:\n- increase global limit with flag -maxConcurrentRequests\n- increase limit with flag: -maxConcurrentPerUserRequests for all users or with config option `max_concurrent_requests` per user.\n- deploy additional vmauth replicas\n- check requests latency at backend service and allocate resources to it if needed",
@@ -998,7 +998,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -1010,7 +1010,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -1022,7 +1022,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -1038,7 +1038,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Shows duration in seconds of user requests by quantile.",
@@ -1124,7 +1124,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -1136,7 +1136,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -1162,7 +1162,7 @@
"panels": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Percentage of used memory (resident).\nThe application's performance will significantly degrade when memory usage is close to 100%.",
@@ -1251,7 +1251,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -1268,7 +1268,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"fieldConfig": {
@@ -1356,7 +1356,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -1375,7 +1375,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "",
@@ -1464,7 +1464,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"expr": "sum(go_memstats_sys_bytes{job=~\"$job\", instance=~\"$instance\"}) + sum(vm_cache_size_bytes{job=~\"$job\", instance=~\"$instance\"})",
@@ -1476,7 +1476,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"expr": "sum(go_memstats_heap_inuse_bytes{job=~\"$job\", instance=~\"$instance\"}) + sum(vm_cache_size_bytes{job=~\"$job\", instance=~\"$instance\"})",
@@ -1488,7 +1488,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"expr": "sum(go_memstats_stack_inuse_bytes{job=~\"$job\", instance=~\"$instance\"})",
@@ -1500,7 +1500,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"expr": "sum(process_resident_memory_bytes{job=~\"$job\", instance=~\"$instance\"})",
@@ -1513,7 +1513,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"exemplar": false,
@@ -1531,7 +1531,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "",
@@ -1636,7 +1636,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"expr": "rate(process_cpu_seconds_total{job=~\"$job\", instance=~\"$instance\"}[$__rate_interval])",
@@ -1648,7 +1648,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"exemplar": false,
@@ -1666,7 +1666,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "",
@@ -1755,7 +1755,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"expr": "sum(vm_tcplistener_conns{job=~\"$job\", instance=~\"$instance\"})",
@@ -1771,7 +1771,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "",
@@ -1860,7 +1860,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -1878,7 +1878,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Shows the percentage of open file descriptors compared to the limit set in the OS.\nReaching the limit of open files can cause various issues and must be prevented.\n\nSee how to change limits here https://medium.com/@muhammadtriwibowo/set-permanently-ulimit-n-open-files-in-ubuntu-4d61064429a",
@@ -1984,7 +1984,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -2003,7 +2003,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"fieldConfig": {
@@ -2092,7 +2092,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"expr": "sum(go_goroutines{job=~\"$job\", instance=~\"$instance\"})",
@@ -2107,7 +2107,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"fieldConfig": {
@@ -2196,7 +2196,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"expr": "sum(process_num_threads{job=~\"$job\", instance=~\"$instance\"})",
@@ -2225,7 +2225,7 @@
"panels": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"fieldConfig": {
@@ -2311,7 +2311,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -2352,7 +2352,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Shows number of generated error and warning messages in logs. Non-zero value may be a sign of connectivity or missconfiguration errors.",
@@ -2433,7 +2433,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -2448,7 +2448,7 @@
},
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"description": "Shows the number of restarts per job. The chart can be useful to identify periodic process restarts and correlate them with potential issues or anomalies. Normally, processes shouldn't restart unless restart was inited by user. The reason of restarts should be figured out by checking the logs of each specific service. ",
@@ -2537,7 +2537,7 @@
"targets": [
{
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"editorMode": "code",
@@ -2573,7 +2573,7 @@
"multi": false,
"name": "ds",
"options": [],
"query": "victoriametrics-metrics-datasource",
"query": "victoriametrics-datasource",
"queryValue": "",
"refresh": 1,
"regex": "",
@@ -2583,7 +2583,7 @@
{
"current": {},
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"definition": "label_values(vm_app_version{version=~\"^vmauth.*\"}, job)",
@@ -2606,7 +2606,7 @@
"allValue": ".*",
"current": {},
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"definition": "label_values(vm_app_version{job=~\"$job\"}, instance)",
@@ -2628,7 +2628,7 @@
{
"current": {},
"datasource": {
"type": "victoriametrics-metrics-datasource",
"type": "victoriametrics-datasource",
"uid": "$ds"
},
"definition": "label_values(vmauth_user_requests_total{job=~\"$job\", instance=~\"$instance\"}, username)",

View File

@@ -2,11 +2,11 @@
DOCKER_NAMESPACE ?= victoriametrics
ROOT_IMAGE ?= alpine:3.21.2
ROOT_IMAGE ?= alpine:3.21.0
ROOT_IMAGE_SCRATCH ?= scratch
CERTS_IMAGE := alpine:3.21.2
CERTS_IMAGE := alpine:3.21.0
GO_BUILDER_IMAGE := golang:1.23.5-alpine
GO_BUILDER_IMAGE := golang:1.23.4-alpine
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

View File

@@ -4,7 +4,7 @@ services:
# And forward them to --remoteWrite.url
vmagent:
container_name: vmagent
image: victoriametrics/vmagent:v1.109.1
image: victoriametrics/vmagent:v1.108.1
depends_on:
- "vminsert"
ports:
@@ -39,7 +39,7 @@ services:
# where N is number of vmstorages (2 in this case).
vmstorage-1:
container_name: vmstorage-1
image: victoriametrics/vmstorage:v1.109.1-cluster
image: victoriametrics/vmstorage:v1.108.1-cluster
ports:
- 8482
- 8400
@@ -51,7 +51,7 @@ services:
restart: always
vmstorage-2:
container_name: vmstorage-2
image: victoriametrics/vmstorage:v1.109.1-cluster
image: victoriametrics/vmstorage:v1.108.1-cluster
ports:
- 8482
- 8400
@@ -66,7 +66,7 @@ services:
# pre-process them and distributes across configured vmstorage shards.
vminsert:
container_name: vminsert
image: victoriametrics/vminsert:v1.109.1-cluster
image: victoriametrics/vminsert:v1.108.1-cluster
depends_on:
- "vmstorage-1"
- "vmstorage-2"
@@ -81,7 +81,7 @@ services:
# vmselect collects results from configured `--storageNode` shards.
vmselect-1:
container_name: vmselect-1
image: victoriametrics/vmselect:v1.109.1-cluster
image: victoriametrics/vmselect:v1.108.1-cluster
depends_on:
- "vmstorage-1"
- "vmstorage-2"
@@ -94,7 +94,7 @@ services:
restart: always
vmselect-2:
container_name: vmselect-2
image: victoriametrics/vmselect:v1.109.1-cluster
image: victoriametrics/vmselect:v1.108.1-cluster
depends_on:
- "vmstorage-1"
- "vmstorage-2"
@@ -112,7 +112,7 @@ services:
# It can be used as an authentication proxy.
vmauth:
container_name: vmauth
image: victoriametrics/vmauth:v1.109.1
image: victoriametrics/vmauth:v1.108.1
depends_on:
- "vmselect-1"
- "vmselect-2"
@@ -127,7 +127,7 @@ services:
# vmalert executes alerting and recording rules
vmalert:
container_name: vmalert
image: victoriametrics/vmalert:v1.109.1
image: victoriametrics/vmalert:v1.108.1
depends_on:
- "vmauth"
ports:

View File

@@ -16,7 +16,7 @@ services:
- ./../../dashboards/victoriametrics.json:/var/lib/grafana/dashboards/vm.json
- ./../../dashboards/victorialogs.json:/var/lib/grafana/dashboards/vl.json
environment:
- "GF_INSTALL_PLUGINS=https://github.com/VictoriaMetrics/victorialogs-datasource/releases/download/v0.13.4/victoriametrics-logs-datasource-v0.13.4.zip;victoriametrics-logs-datasource"
- "GF_INSTALL_PLUGINS=https://github.com/VictoriaMetrics/victorialogs-datasource/releases/download/v0.13.0/victoriametrics-logs-datasource-v0.13.0.zip;victoriametrics-logs-datasource"
- "GF_PLUGINS_ALLOW_LOADING_UNSIGNED_PLUGINS=victoriametrics-logs-datasource"
networks:
- vm_net
@@ -45,7 +45,7 @@ services:
# storing logs and serving read queries.
victorialogs:
container_name: victorialogs
image: victoriametrics/victoria-logs:v1.7.0-victorialogs
image: victoriametrics/victoria-logs:v1.4.0-victorialogs
command:
- "--storageDataPath=/vlogs"
- "--httpListenAddr=:9428"
@@ -60,7 +60,7 @@ services:
# scraping, storing metrics and serve read requests.
victoriametrics:
container_name: victoriametrics
image: victoriametrics/victoria-metrics:v1.109.1
image: victoriametrics/victoria-metrics:v1.108.1
ports:
- 8428:8428
volumes:
@@ -79,7 +79,7 @@ services:
# depending on the requested path.
vmauth:
container_name: vmauth
image: victoriametrics/vmauth:v1.109.1
image: victoriametrics/vmauth:v1.108.1
depends_on:
- "victoriametrics"
- "victorialogs"
@@ -96,7 +96,7 @@ services:
# vmalert executes alerting and recording rules according to given rule type.
vmalert:
container_name: vmalert
image: victoriametrics/vmalert:v1.109.1
image: victoriametrics/vmalert:v1.108.1
depends_on:
- "vmauth"
- "alertmanager"

View File

@@ -4,7 +4,7 @@ services:
# And forward them to --remoteWrite.url
vmagent:
container_name: vmagent
image: victoriametrics/vmagent:v1.109.1
image: victoriametrics/vmagent:v1.108.1
depends_on:
- "victoriametrics"
ports:
@@ -22,7 +22,7 @@ services:
# storing metrics and serve read requests.
victoriametrics:
container_name: victoriametrics
image: victoriametrics/victoria-metrics:v1.109.1
image: victoriametrics/victoria-metrics:v1.108.1
ports:
- 8428:8428
- 8089:8089
@@ -65,7 +65,7 @@ services:
# vmalert executes alerting and recording rules
vmalert:
container_name: vmalert
image: victoriametrics/vmalert:v1.109.1
image: victoriametrics/vmalert:v1.108.1
depends_on:
- "victoriametrics"
- "alertmanager"

View File

@@ -2,7 +2,7 @@ apiVersion: 1
datasources:
- name: VictoriaMetrics - cluster
type: victoriametrics-metrics-datasource
type: victoriametrics-datasource
access: proxy
url: http://vmauth:8427/select/0/prometheus
isDefault: true

View File

@@ -2,7 +2,7 @@ apiVersion: 1
datasources:
- name: VictoriaMetrics
type: victoriametrics-metrics-datasource
type: victoriametrics-datasource
access: proxy
url: http://victoriametrics:8428
isDefault: true

View File

@@ -1,7 +1,7 @@
services:
# meta service will be ignored by compose
.victorialogs:
image: docker.io/victoriametrics/victoria-logs:v1.7.0-victorialogs
image: docker.io/victoriametrics/victoria-logs:v1.4.0-victorialogs
command:
- -storageDataPath=/vlogs
- -loggerFormat=json
@@ -19,7 +19,7 @@ services:
retries: 10
dd-proxy:
image: docker.io/victoriametrics/vmauth:v1.109.1
image: docker.io/victoriametrics/vmauth:v1.108.1
restart: on-failure
volumes:
- ./:/etc/vmauth
@@ -45,7 +45,7 @@ services:
replicas: 0
victoriametrics:
image: victoriametrics/victoria-metrics:v1.109.1
image: victoriametrics/victoria-metrics:latest
ports:
- '8428:8428'
command:

View File

@@ -16,6 +16,6 @@ services:
- ./../../dashboards/vm/vmalert.json:/var/lib/grafana/dashboards/vmalert.json
- ./../../dashboards/vm/vmauth.json:/var/lib/grafana/dashboards/vmauth.json
environment:
- "GF_INSTALL_PLUGINS=https://github.com/VictoriaMetrics/victoriametrics-datasource/releases/download/v0.12.1/victoriametrics-metrics-datasource-v0.12.1.zip;victoriametrics-metrics-datasource"
- "GF_PLUGINS_ALLOW_LOADING_UNSIGNED_PLUGINS=victoriametrics-metrics-datasource"
- "GF_INSTALL_PLUGINS=https://github.com/VictoriaMetrics/victoriametrics-datasource/releases/download/v0.10.3/victoriametrics-datasource-v0.10.3.zip;victoriametrics-datasource"
- "GF_PLUGINS_ALLOW_LOADING_UNSIGNED_PLUGINS=victoriametrics-datasource"
restart: always

View File

@@ -15,8 +15,8 @@ services:
- ./../../dashboards/vm/vmagent.json:/var/lib/grafana/dashboards/vmagent.json
- ./../../dashboards/vm/vmalert.json:/var/lib/grafana/dashboards/vmalert.json
environment:
- "GF_INSTALL_PLUGINS=https://github.com/VictoriaMetrics/victoriametrics-datasource/releases/download/v0.12.1/victoriametrics-metrics-datasource-v0.12.1.zip;victoriametrics-metrics-datasource"
- "GF_PLUGINS_ALLOW_LOADING_UNSIGNED_PLUGINS=victoriametrics-metrics-datasource"
- "GF_INSTALL_PLUGINS=https://github.com/VictoriaMetrics/victoriametrics-datasource/releases/download/v0.10.3/victoriametrics-datasource-v0.10.3.zip;victoriametrics-datasource"
- "GF_PLUGINS_ALLOW_LOADING_UNSIGNED_PLUGINS=victoriametrics-datasource"
networks:
- vm_net
restart: always

View File

@@ -1,7 +1,7 @@
services:
vmagent:
container_name: vmagent
image: victoriametrics/vmagent:v1.109.1
image: victoriametrics/vmagent:v1.108.1
depends_on:
- "victoriametrics"
ports:
@@ -18,7 +18,7 @@ services:
victoriametrics:
container_name: victoriametrics
image: victoriametrics/victoria-metrics:v1.109.1
image: victoriametrics/victoria-metrics:v1.108.1
ports:
- 8428:8428
volumes:
@@ -50,7 +50,7 @@ services:
vmalert:
container_name: vmalert
image: victoriametrics/vmalert:v1.109.1
image: victoriametrics/vmalert:v1.108.1
depends_on:
- "victoriametrics"
ports:
@@ -72,7 +72,7 @@ services:
restart: always
vmanomaly:
container_name: vmanomaly
image: victoriametrics/vmanomaly:v1.19.1
image: victoriametrics/vmanomaly:v1.18.8
depends_on:
- "victoriametrics"
ports:

View File

@@ -1,23 +1,21 @@
schedulers:
periodic:
# class: "scheduler.periodic.PeriodicScheduler"
infer_every: "1m"
fit_every: "1h"
fit_window: "2d" # 2d-14d based on the presense of weekly seasonality in your data
fit_every: "2m"
fit_window: "3h"
models:
prophet:
class: "prophet"
class: "model.prophet.ProphetModel"
args:
interval_width: 0.98
weekly_seasonality: False # comment it if your data has weekly seasonality
yearly_seasonality: False
reader:
datasource_url: "http://victoriametrics:8428/"
sampling_period: "60s"
sampling_period: "60s"
queries:
node_cpu_rate:
expr: "sum(rate(node_cpu_seconds_total[5m])) by (mode, instance, job)"
node_cpu_rate: "sum(rate(node_cpu_seconds_total[5m])) by (mode, instance, job)"
writer:
datasource_url: "http://victoriametrics:8428/"
@@ -26,4 +24,4 @@ writer:
monitoring:
pull: # Enable /metrics endpoint.
addr: "0.0.0.0"
port: 8490
port: 8490

View File

@@ -18,7 +18,7 @@ services:
- vlogs
generator:
image: golang:1.23.5-alpine
image: golang:1.23.4-alpine
restart: always
working_dir: /go/src/app
volumes:

View File

@@ -2,7 +2,7 @@ version: '3'
services:
generator:
image: golang:1.23.5-alpine
image: golang:1.23.4-alpine
restart: always
working_dir: /go/src/app
volumes:

View File

@@ -3,7 +3,7 @@ version: "3"
services:
# Run `make package-victoria-logs` to build victoria-logs image
vlogs:
image: docker.io/victoriametrics/victoria-logs:v1.7.0-victorialogs
image: docker.io/victoriametrics/victoria-logs:v1.4.0-victorialogs
volumes:
- vlogs:/vlogs
ports:
@@ -46,7 +46,7 @@ services:
- "--config=/config.yml"
vmsingle:
image: victoriametrics/victoria-metrics:v1.109.0
image: victoriametrics/victoria-metrics:v1.108.1
ports:
- "8428:8428"
command:

View File

@@ -761,8 +761,8 @@ Some workloads may need fine-grained resource usage limits. In these cases the f
- `-search.maxDeleteSeries` at `vmselect` limits the number of unique time
series that can be deleted by a single
[/api/v1/admin/tsdb/delete_series](https://docs.victoriametrics.com/url-examples/#apiv1admintsdbdelete_series)
call. The duration is limited via `-search.maxDeleteDuration` flag{{% available_from "#tip" %}}. Deleting too many time series may require big
amount of CPU and memory at `vmstorage` and this limit guards against unplanned resource usage spikes.
call. Deleting too many time series may require big amount of CPU and memory
at `vmstorage` and this limit guards against unplanned resource usage spikes.
Also see [How to delete time series](#how-to-delete-time-series) section to
learn about different ways of deleting series.
- `-search.maxTagKeys` at `vmstorage` limits the number of items, which may be returned from
@@ -1250,7 +1250,7 @@ Below is the output for `/path/to/vminsert -help`:
-loggerWarnsPerSecondLimit int
Per-second limit on the number of WARN messages. If more than the given number of warns are emitted per second, then the remaining warns are suppressed. Zero values disable the rate limit
-maxConcurrentInserts int
The maximum number of concurrent insert requests. Set higher value when clients send data over slow networks. Default value depends on the number of available CPU cores. It should work fine in most cases since it minimizes resource usage. See also -insert.maxQueueDuration
The maximum number of concurrent insert requests. Set higher value when clients send data over slow networks. Default value depends on the number of available CPU cores. It should work fine in most cases since it minimizes resource usage. See also -insert.maxQueueDuration (default 32)
-maxInsertRequestSize size
The maximum size in bytes of a single Prometheus remote_write API request
Supports the following optional suffixes for size values: KB, MB, GB, TB, KiB, MiB, GiB, TiB (default 33554432)
@@ -1397,7 +1397,7 @@ Below is the output for `/path/to/vmselect -help`:
-clusternative.disableCompression
Whether to disable compression of the data sent to vmselect via -clusternativeListenAddr. This reduces CPU usage at the cost of higher network bandwidth usage
-clusternative.maxConcurrentRequests int
The maximum number of concurrent vmselect requests the server can process at -clusternativeListenAddr. It shouldn't be high, since a single request usually saturates a CPU core at the underlying vmstorage nodes, and many concurrently executed requests may require high amounts of memory. See also -clusternative.maxQueueDuration
The maximum number of concurrent vmselect requests the server can process at -clusternativeListenAddr. It shouldn't be high, since a single request usually saturates a CPU core at the underlying vmstorage nodes, and many concurrently executed requests may require high amounts of memory. See also -clusternative.maxQueueDuration (default 32)
-clusternative.maxQueueDuration duration
The maximum time the incoming query to -clusternativeListenAddr waits for execution when -clusternative.maxConcurrentRequests limit is reached (default 10s)
-clusternative.maxTagKeys int
@@ -1425,7 +1425,7 @@ Below is the output for `/path/to/vmselect -help`:
-dedup.minScrapeInterval duration
Leave only the last sample in every time series per each discrete interval equal to -dedup.minScrapeInterval > 0. See https://docs.victoriametrics.com/#deduplication for details
-deleteAuthKey value
authKey for metrics' deletion via /prometheus/api/v1/admin/tsdb/delete_series and /graphite/tags/delSeries. It could be passed via authKey query arg.
authKey for metrics' deletion via /prometheus/api/v1/admin/tsdb/delete_series and /graphite/tags/delSeries
Flag value can be read from the given file when using -deleteAuthKey=file:///abs/path/to/file or -deleteAuthKey=file://./relative/path/to/file . Flag value can be read from the given http/https url when using -deleteAuthKey=http://host/path or -deleteAuthKey=https://host/path
-denyQueryTracing
Whether to disable the ability to trace queries. See https://docs.victoriametrics.com/#query-tracing
@@ -1581,8 +1581,6 @@ Below is the output for `/path/to/vmselect -help`:
A larger value makes the pushed-down filter more complex but fewer time series will be returned. This flag is useful when selective label contains numerous values, for example `instance`, and storage resources are abundant. (default 100)
-search.maxConcurrentRequests int
The maximum number of concurrent search requests. It shouldn't be high, since a single request can saturate all the CPU cores, while many concurrently executed requests may require high amounts of memory. See also -search.maxQueueDuration and -search.maxMemoryPerQuery (default 16)
-search.maxDeleteDuration duration
The maximum duration for /api/v1/admin/tsdb/delete_series call (default 5m)
-search.maxDeleteSeries int
The maximum number of time series, which can be deleted using /api/v1/admin/tsdb/delete_series. This option allows limiting memory usage (default 1000000)
-search.maxExportDuration duration
@@ -1653,7 +1651,7 @@ Below is the output for `/path/to/vmselect -help`:
-search.queryStats.minQueryDuration duration
The minimum duration for queries to track in query stats at /api/v1/status/top_queries. Queries with lower duration are ignored in query stats (default 1ms)
-search.resetCacheAuthKey value
Optional authKey for resetting rollup cache via /internal/resetRollupResultCache call. It could be passed via authKey query arg.
Optional authKey for resetting rollup cache via /internal/resetRollupResultCache call
Flag value can be read from the given file when using -search.resetCacheAuthKey=file:///abs/path/to/file or -search.resetCacheAuthKey=file://./relative/path/to/file . Flag value can be read from the given http/https url when using -search.resetCacheAuthKey=http://host/path or -search.resetCacheAuthKey=https://host/path
-search.resetRollupResultCacheOnStartup
Whether to reset rollup result cache on startup. See https://docs.victoriametrics.com/#rollup-result-cache . See also -search.disableCache
@@ -1848,7 +1846,7 @@ Below is the output for `/path/to/vmstorage -help`:
-loggerWarnsPerSecondLimit int
Per-second limit on the number of WARN messages. If more than the given number of warns are emitted per second, then the remaining warns are suppressed. Zero values disable the rate limit
-maxConcurrentInserts int
The maximum number of concurrent insert requests. Set higher value when clients send data over slow networks. Default value depends on the number of available CPU cores. It should work fine in most cases since it minimizes resource usage. See also -insert.maxQueueDuration
The maximum number of concurrent insert requests. Set higher value when clients send data over slow networks. Default value depends on the number of available CPU cores. It should work fine in most cases since it minimizes resource usage. See also -insert.maxQueueDuration (default 32)
-memory.allowedBytes size
Allowed size of system memory VictoriaMetrics caches may occupy. This option overrides -memory.allowedPercent if set to a non-zero value. Too low a value may increase the cache miss rate usually resulting in higher CPU and disk IO usage. Too high a value may evict too much data from the OS page cache resulting in higher disk IO usage
Supports the following optional suffixes for size values: KB, MB, GB, TB, KiB, MiB, GiB, TiB (default 0)
@@ -1902,7 +1900,7 @@ Below is the output for `/path/to/vmstorage -help`:
-rpc.disableCompression
Whether to disable compression of the data sent from vmstorage to vmselect. This reduces CPU usage at the cost of higher network bandwidth usage
-search.maxConcurrentRequests int
The maximum number of concurrent vmselect requests the vmstorage can process at -vmselectAddr. It shouldn't be high, since a single request usually saturates a CPU core, and many concurrently executed requests may require high amounts of memory. See also -search.maxQueueDuration
The maximum number of concurrent vmselect requests the vmstorage can process at -vmselectAddr. It shouldn't be high, since a single request usually saturates a CPU core, and many concurrently executed requests may require high amounts of memory. See also -search.maxQueueDuration (default 32)
-search.maxQueueDuration duration
The maximum time the incoming vmselect request waits for execution when -search.maxConcurrentRequests limit is reached (default 10s)
-search.maxTagKeys int

View File

@@ -22,5 +22,5 @@ to [the latest available releases](https://docs.victoriametrics.com/changelog/).
## Currently supported LTS release lines
- v1.102.x - the latest one is [v1.102.10 LTS release](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.102.10)
- v1.97.x - the latest one is [v1.97.15 LTS release](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.97.15)
- v1.102.x - the latest one is [v1.102.9 LTS release](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.102.9)
- v1.97.x - the latest one is [v1.97.14 LTS release](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.97.14)

Some files were not shown because too many files have changed in this diff Show More