mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2026-05-20 02:06:31 +03:00
Compare commits
2 Commits
test-templ
...
improve-te
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72eb3c6cf6 | ||
|
|
2489095595 |
2
Makefile
2
Makefile
@@ -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`
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
15
app/vmalert-tool/unittest/testdata/test1.yaml
vendored
15
app/vmalert-tool/unittest/testdata/test1.yaml
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}()
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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{"label"="bar"}<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": "
|
||||
|
||||
// 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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
553
app/vmalert/templates/init.go
Normal file
553
app/vmalert/templates/init.go
Normal 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)
|
||||
}
|
||||
}
|
||||
261
app/vmalert/templates/init_test.go
Normal file
261
app/vmalert/templates/init_test.go
Normal 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 < 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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 < 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{"label"="bar"}<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": "
|
||||
|
||||
// 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",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1685,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 {
|
||||
@@ -2195,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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
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.4-alpine
|
||||
BUILDER_IMAGE := local/builder:2.0.0-$(shell echo $(GO_BUILDER_IMAGE) | tr :/ __)-1
|
||||
|
||||
@@ -4,7 +4,7 @@ services:
|
||||
# And forward them to --remoteWrite.url
|
||||
vmagent:
|
||||
container_name: vmagent
|
||||
image: victoriametrics/vmagent:v1.109.0
|
||||
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.0-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.0-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.0-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.0-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.0-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.0
|
||||
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.0
|
||||
image: victoriametrics/vmalert:v1.108.1
|
||||
depends_on:
|
||||
- "vmauth"
|
||||
ports:
|
||||
|
||||
@@ -45,7 +45,7 @@ services:
|
||||
# storing logs and serving read queries.
|
||||
victorialogs:
|
||||
container_name: victorialogs
|
||||
image: victoriametrics/victoria-logs:v1.6.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.0
|
||||
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.0
|
||||
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.0
|
||||
image: victoriametrics/vmalert:v1.108.1
|
||||
depends_on:
|
||||
- "vmauth"
|
||||
- "alertmanager"
|
||||
|
||||
@@ -4,7 +4,7 @@ services:
|
||||
# And forward them to --remoteWrite.url
|
||||
vmagent:
|
||||
container_name: vmagent
|
||||
image: victoriametrics/vmagent:v1.109.0
|
||||
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.0
|
||||
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.0
|
||||
image: victoriametrics/vmalert:v1.108.1
|
||||
depends_on:
|
||||
- "victoriametrics"
|
||||
- "alertmanager"
|
||||
|
||||
@@ -3,16 +3,6 @@
|
||||
# The alerts below are just recommendations and may require some updates
|
||||
# and threshold calibration according to every specific setup.
|
||||
groups:
|
||||
- name: alwayFiring
|
||||
rules:
|
||||
- alert: AlwaysFiring
|
||||
expr: up==1
|
||||
labels:
|
||||
severity: critical
|
||||
annotations:
|
||||
summary: "job {{ $labels.job }} has an instance {{ $labels.instance | stripPort }}"
|
||||
instance: "{{ $labels.instance }}"
|
||||
description: "This alert is fine"
|
||||
- name: vm-health
|
||||
# note the `job` filter and update accordingly to your setup
|
||||
rules:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
services:
|
||||
# meta service will be ignored by compose
|
||||
.victorialogs:
|
||||
image: docker.io/victoriametrics/victoria-logs:v1.6.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.0
|
||||
image: docker.io/victoriametrics/vmauth:v1.108.1
|
||||
restart: on-failure
|
||||
volumes:
|
||||
- ./:/etc/vmauth
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
services:
|
||||
vmagent:
|
||||
container_name: vmagent
|
||||
image: victoriametrics/vmagent:v1.109.0
|
||||
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.0
|
||||
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.0
|
||||
image: victoriametrics/vmalert:v1.108.1
|
||||
depends_on:
|
||||
- "victoriametrics"
|
||||
ports:
|
||||
|
||||
@@ -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.6.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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -70,7 +70,6 @@ Bumping the limits may significantly improve build speed.
|
||||
* linux/386
|
||||
This step can be run manually with the command `make publish` from the needed git tag.
|
||||
1. Verify that created images are stable and don't introduce regressions on [test environment](https://github.com/VictoriaMetrics/VictoriaMetrics-enterprise/blob/master/Release-Guide.md#testing-releases).
|
||||
1. Test new images on [sandbox](https://github.com/VictoriaMetrics/VictoriaMetrics-enterprise/blob/master/Release-Guide.md#testing-releases).
|
||||
1. Push the tags `v1.xx.y` and `v1.xx.y-cluster` created at previous steps to public GitHub repository at https://github.com/VictoriaMetrics/VictoriaMetrics.
|
||||
Push the tags `v1.xx.y`, `v1.xx.y-cluster`, `v1.xx.y-enterprise` and `v1.xx.y-enterprise-cluster` to the corresponding
|
||||
branches in private repository.
|
||||
@@ -89,6 +88,7 @@ Bumping the limits may significantly improve build speed.
|
||||
file created at the step `a`.
|
||||
- To run the command `TAG=v1.xx.y make github-create-release github-upload-assets`, so new release is created
|
||||
and all the needed assets are re-uploaded to it.
|
||||
1. Test new images on [sandbox](https://github.com/VictoriaMetrics/VictoriaMetrics-enterprise/blob/master/Release-Guide.md#testing-releases).
|
||||
1. Go to <https://github.com/VictoriaMetrics/VictoriaMetrics/releases> and verify that draft release with the name `TAG` has been created
|
||||
and this release contains all the needed binaries and checksums.
|
||||
1. Update the release description with the content of [CHANGELOG](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/docs/CHANGELOG.md) for this release.
|
||||
|
||||
@@ -16,18 +16,6 @@ according to [these docs](https://docs.victoriametrics.com/victorialogs/quicksta
|
||||
|
||||
## tip
|
||||
|
||||
## [v1.6.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.6.0-victorialogs)
|
||||
|
||||
Released at 2025-01-15
|
||||
|
||||
* FEATURE: add [`union` pipe](https://docs.victoriametrics.com/victorialogs/logsql/#union-pipe), which can be used for returning results from multiple independent LogsQL queries.
|
||||
* FEATURE: add [`histogram` stats function](https://docs.victoriametrics.com/victorialogs/logsql/#histogram-stats) for calculating [VictoriaMetrics histogram buckets](https://valyala.medium.com/improving-histogram-usability-for-prometheus-and-grafana-bc7e5df0e350) over the given [log field](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model). They will be used for building heatmaps at the [built-in Web UI](https://docs.victoriametrics.com/victorialogs/querying/#web-ui) and [VictoriaLogs plugin for Grafana](https://docs.victoriametrics.com/victorialogs/victorialogs-datasource/).
|
||||
* FEATURE: [`math` pipe](https://docs.victoriametrics.com/victorialogs/logsql/#math-pipe): add `rand()` function, which can be used for generating random numbers in the range `[0 ... 1)`.
|
||||
|
||||
## [v1.5.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.5.0-victorialogs)
|
||||
|
||||
Released at 2025-01-13
|
||||
|
||||
* FEATURE: [`count_uniq` stats function](https://docs.victoriametrics.com/victorialogs/logsql/#count_uniq-stats): improve performance by up to 50% and reduce memory usage by up to 4x when this function is applied to fields with big number of unique integer values.
|
||||
* FEATURE: [`stats` pipe](https://docs.victoriametrics.com/victorialogs/logsql/#stats-pipe): improve performance and reduce memory usage by up to 50% for `log_field` with big number of unique values at `stats by (log_field) ...`.
|
||||
* FEATURE: [`math` pipe](https://docs.victoriametrics.com/victorialogs/logsql/#math-pipe): improve performance by up to 8x.
|
||||
|
||||
@@ -139,7 +139,7 @@ _time:5m error -(buggy_app OR foobar)
|
||||
|
||||
The parentheses are **required** here, since otherwise the query won't return the expected results.
|
||||
The query `error -buggy_app OR foobar` is interpreted as `(error AND NOT buggy_app) OR foobar` according to [priorities for AND, OR and NOT operator](#logical-filters).
|
||||
This query returns logs with `foobar` [word](#word), even if they do not contain `error` word or contain `buggy_app` word.
|
||||
This query returns logs with `foobar` [word](#word), even if do not contain `error` word or contain `buggy_app` word.
|
||||
So it is recommended wrapping the needed query parts into explicit parentheses if you are unsure in priority rules.
|
||||
As an additional bonus, explicit parentheses make queries easier to read and maintain.
|
||||
|
||||
@@ -1344,7 +1344,6 @@ LogsQL supports the following pipes:
|
||||
- [`stream_context`](#stream_context-pipe) allows selecting surrounding logs in front and after the matching logs
|
||||
per each [log stream](https://docs.victoriametrics.com/victorialogs/keyconcepts/#stream-fields).
|
||||
- [`top`](#top-pipe) returns top `N` field sets with the maximum number of matching logs.
|
||||
- [`union`](#union-pipe) returns results from multiple LogsQL queries.
|
||||
- [`uniq`](#uniq-pipe) returns unique log entires.
|
||||
- [`unpack_json`](#unpack_json-pipe) unpacks JSON messages from [log fields](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model).
|
||||
- [`unpack_logfmt`](#unpack_logfmt-pipe) unpacks [logfmt](https://brandur.org/logfmt) messages from [log fields](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model).
|
||||
@@ -1353,18 +1352,18 @@ LogsQL supports the following pipes:
|
||||
|
||||
### block_stats pipe
|
||||
|
||||
`<q> | block_stats` [pipe](#pipes) returns the following stats per each block processed by `<q>` [query](#query-syntax):
|
||||
`<q> | block_stats` [pipe](#pipes) returns the following stats per each block processed by `<q>`. This pipe is needed mostly for debugging.
|
||||
|
||||
The returned per-block stats:
|
||||
|
||||
- `field` - [field](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model) name
|
||||
- `rows` - the number of rows at the given `field`
|
||||
- `rows` - the number of rows at the given `field.
|
||||
- `type` - internal storage type for the given `field`
|
||||
- `values_bytes` - on-disk size of the data for the given `field`
|
||||
- `bloom_bytes` - on-disk size of bloom filter data for the given `field`
|
||||
- `dict_bytes` - on-disk size of the dictionary data for the given `field`
|
||||
- `dict_items` - the number of unique values in the dictionary for the given `field`
|
||||
|
||||
The `block_stats` pipe is needed mostly for debugging purposes.
|
||||
|
||||
See also:
|
||||
|
||||
- [`value_type` filter](#value_type-filter)
|
||||
@@ -1508,10 +1507,10 @@ See also:
|
||||
|
||||
For example, the following query selects logs with the `error` [word](#word) for the last day,
|
||||
extracts ip address from [`_msg` field](https://docs.victoriametrics.com/victorialogs/keyconcepts/#message-field) into `ip` field and then calculates top 10 ip addresses
|
||||
with the biggest number of logs using [`top` pipe](#top-pipe):
|
||||
with the biggest number of logs:
|
||||
|
||||
```logsql
|
||||
_time:1d error | extract "ip=<ip> " from _msg | top 10 (ip)
|
||||
_time:1d error | extract "ip=<ip> " from _msg | stats by (ip) count() logs | sort by (logs) desc limit 10
|
||||
```
|
||||
|
||||
It is expected that `_msg` field contains `ip=...` substring ending with space. For example, `error ip=1.2.3.4 from user_id=42`.
|
||||
@@ -1521,7 +1520,7 @@ If the `| extract ...` pipe is applied to [`_msg` field](https://docs.victoriame
|
||||
For example, the following query is equivalent to the previous one:
|
||||
|
||||
```logsql
|
||||
_time:1d error | extract "ip=<ip> " | top 10 (ip)
|
||||
_time:1d error | extract "ip=<ip> " | stats by (ip) count() logs | sort by (logs) desc limit 10
|
||||
```
|
||||
|
||||
If the `pattern` contains double quotes, then either put `\` in front of double quotes or put the `pattern` inside single quotes.
|
||||
@@ -1941,7 +1940,7 @@ Numeric fields can be transformed into the following string representation at `f
|
||||
- IPv4 - by adding `ipv4:` in front of the corresponding field name containing `uint32` representation of the IPv4 address.
|
||||
For example, `format "ip=<ipv4:ip_num>"`.
|
||||
|
||||
- Zero-padded 64-bit hex number - by adding `hexnumencode:` in front of the corresponding field name. For example, `format "hex_num=<hexnumencode:some_field>"`.
|
||||
- Zero-padded 64-bit hex number - by adding 'hexnumencode:' in front of the corresponding field name. For example, `format "hex_num=<hexnumencode:some_field>"`.
|
||||
|
||||
Add `keep_original_fields` to the end of `format ... as result_field` when the original non-empty value of the `result_field` must be preserved
|
||||
instead of overwriting it with the `format` results. For example, the following query adds formatted result to `foo` field only if it was missing or empty:
|
||||
@@ -2029,7 +2028,6 @@ _time:1d {app="app1"} | stats by (user) count() app1_hits
|
||||
|
||||
See also:
|
||||
|
||||
- [`in` filter](#multi-exact-filter)
|
||||
- [`stats` pipe](#stats-pipe)
|
||||
- [conditional `stats`](https://docs.victoriametrics.com/victorialogs/logsql/#stats-with-additional-filters)
|
||||
- [`filter` pipe](#filter-pipe)
|
||||
@@ -2065,7 +2063,7 @@ For example, the following query shows top 5 log entries with the maximum byte l
|
||||
logs for the last 5 minutes:
|
||||
|
||||
```logsql
|
||||
_time:5m | len(_msg) as msg_len | sort by (msg_len desc) | limit 5
|
||||
_time:5m | len(_msg) as msg_len | sort by (msg_len desc) | limit 1
|
||||
```
|
||||
|
||||
See also:
|
||||
@@ -2143,7 +2141,6 @@ The following mathematical operations are supported by `math` pipe:
|
||||
- `ln(arg)` - returns [natural logarithm](https://en.wikipedia.org/wiki/Natural_logarithm) for the given `arg`
|
||||
- `max(arg1, ..., argN)` - returns the maximum value among the given `arg1`, ..., `argN`
|
||||
- `min(arg1, ..., argN)` - returns the minimum value among the given `arg1`, ..., `argN`
|
||||
- `rand()` - returns pseudo-random number in the range `[0...1)`.
|
||||
- `round(arg)` - returns rounded to integer value for the given `arg`. The `round()` accepts optional `nearest` arg, which allows rounding the number to the given `nearest` multiple.
|
||||
For example, `round(temperature, 0.1)` rounds `temperature` field to one decimal digit after the point.
|
||||
|
||||
@@ -2765,22 +2762,6 @@ See also:
|
||||
- [`uniq` pipe](#uniq-pipe)
|
||||
- [`stats` pipe](#stats-pipe)
|
||||
- [`sort` pipe](#sort-pipe)
|
||||
- [`histogram` stats function](#histogram-stats)
|
||||
|
||||
### union pipe
|
||||
|
||||
`q1 | union (q2)` [pipe](#pipes) returns results of `q1` followed by results of `q2`. It works similar to `UNION ALL` in SQL.
|
||||
`q1` and `q2` may contain arbitrary [LogsQL queries](#logsql-tutorial).
|
||||
For example, the following query returns logs with `error` [word](#word) for the last 5 minutes, plus logs with `panic` word for the last hour:
|
||||
|
||||
```logsql
|
||||
_time:5m error | union (_time:1h panic)
|
||||
```
|
||||
|
||||
See also:
|
||||
|
||||
- [`join` pipe](#join-pipe)
|
||||
- [`in` filter](#multi-exact-filter)
|
||||
|
||||
### uniq pipe
|
||||
|
||||
@@ -3125,7 +3106,6 @@ LogsQL supports the following functions for [`stats` pipe](#stats-pipe):
|
||||
- [`count_empty`](#count_empty-stats) returns the number logs with empty [log fields](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model).
|
||||
- [`count_uniq`](#count_uniq-stats) returns the number of unique non-empty values for the given [log fields](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model).
|
||||
- [`count_uniq_hash`](#count_uniq_hash-stats) returns the number of unique hashes for non-empty values at the given [log fields](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model).
|
||||
- [`histogram`](#histogram-stats) returns [VictoriaMetrics histogram](https://valyala.medium.com/improving-histogram-usability-for-prometheus-and-grafana-bc7e5df0e350) for the given [log field](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model).
|
||||
- [`max`](#max-stats) returns the maximum value over the given [log fields](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model).
|
||||
- [`median`](#median-stats) returns the [median](https://en.wikipedia.org/wiki/Median) value over the given [log fields](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model).
|
||||
- [`min`](#min-stats) returns the minimum value over the given [log fields](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model).
|
||||
@@ -3273,25 +3253,6 @@ See also:
|
||||
- [`uniq_values`](#uniq_values-stats)
|
||||
- [`count`](#count-stats)
|
||||
|
||||
### histogram stats
|
||||
|
||||
`histogram(field)` [stats pipe function](#stats-pipe-functions) returns [VictoriaMetrics histogram buckets](https://valyala.medium.com/improving-histogram-usability-for-prometheus-and-grafana-bc7e5df0e350)
|
||||
for the given [`field`](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model).
|
||||
|
||||
For example, the following query returns histogram buckets for the `response_size` field grouped by `host` field, across logs for the last 5 minutes:
|
||||
|
||||
```logsql
|
||||
_time:5m | stats by (host) histogram(response_size)
|
||||
```
|
||||
|
||||
If the field contains [duration value](#duration-values), then `histogram` normalizes it to nanoseconds. For example, `1.25ms` is normalized to `1_250_000`.
|
||||
|
||||
If the field contains [short numeric value](#short-numeric-values), then `histogram` normalizes it to numeric value without any suffixes. For example, `1KiB` is converted to `1024`.
|
||||
|
||||
See also:
|
||||
|
||||
- [`quantile`](#quantile-stats)
|
||||
|
||||
### max stats
|
||||
|
||||
`max(field1, ..., fieldN)` [stats pipe function](#stats-pipe-functions) returns the maximum value across
|
||||
@@ -3369,7 +3330,6 @@ _time:5m | stats
|
||||
|
||||
See also:
|
||||
|
||||
- [`histogram`](#histogram-stats)
|
||||
- [`min`](#min-stats)
|
||||
- [`max`](#max-stats)
|
||||
- [`median`](#median-stats)
|
||||
|
||||
@@ -33,8 +33,8 @@ Just download archive for the needed Operating system and architecture, unpack i
|
||||
For example, the following commands download VictoriaLogs archive for Linux/amd64, unpack and run it:
|
||||
|
||||
```sh
|
||||
curl -L -O https://github.com/VictoriaMetrics/VictoriaMetrics/releases/download/v1.6.0-victorialogs/victoria-logs-linux-amd64-v1.6.0-victorialogs.tar.gz
|
||||
tar xzf victoria-logs-linux-amd64-v1.6.0-victorialogs.tar.gz
|
||||
curl -L -O https://github.com/VictoriaMetrics/VictoriaMetrics/releases/download/v1.4.0-victorialogs/victoria-logs-linux-amd64-v1.4.0-victorialogs.tar.gz
|
||||
tar xzf victoria-logs-linux-amd64-v1.4.0-victorialogs.tar.gz
|
||||
./victoria-logs-prod
|
||||
```
|
||||
|
||||
@@ -58,7 +58,7 @@ Here is the command to run VictoriaLogs in a Docker container:
|
||||
|
||||
```sh
|
||||
docker run --rm -it -p 9428:9428 -v ./victoria-logs-data:/victoria-logs-data \
|
||||
docker.io/victoriametrics/victoria-logs:v1.6.0-victorialogs
|
||||
docker.io/victoriametrics/victoria-logs:v1.4.0-victorialogs
|
||||
```
|
||||
|
||||
See also:
|
||||
|
||||
@@ -24,11 +24,11 @@ the returned logs by some field (usually [`_time` field](https://docs.victoriame
|
||||
_time:5m | sort by (_time)
|
||||
```
|
||||
|
||||
If the number of returned logs is too big, it may be limited with the [`first` pipe](https://docs.victoriametrics.com/victorialogs/logsql/#first-pipe).
|
||||
If the number of returned logs is too big, it may be limited with the [`last` pipe](https://docs.victoriametrics.com/victorialogs/logsql/#last-pipe).
|
||||
For example, the following query returns 10 most recent logs, which were ingested during the last 5 minutes:
|
||||
|
||||
```logsql
|
||||
_time:5m | first 10 by (_time desc)
|
||||
_time:5m | last 10 by (_time)
|
||||
```
|
||||
|
||||
See also:
|
||||
@@ -358,7 +358,7 @@ query returns top 10 `/24` subnetworks with the biggest number of logs for the l
|
||||
_time:5m | stats by (ip:/24) count() rows | last 10 by (rows)
|
||||
```
|
||||
|
||||
This query uses [`first` pipe](https://docs.victoriametrics.com/victorialogs/logsql/#first-pipe) in order to get up to 10 per-subnetwork stats
|
||||
This query uses [`last` pipe](https://docs.victoriametrics.com/victorialogs/logsql/#last-pipe) in order to get up to 10 per-subnetwork stats
|
||||
with the biggest number of rows.
|
||||
|
||||
The query assumes the original logs have `ip` [field](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model) with the IPv4 address.
|
||||
@@ -369,7 +369,7 @@ extracts IPv4 address from [`_msg` field](https://docs.victoriametrics.com/victo
|
||||
`/16` subnetworks with the biggest number of logs for the last 5 minutes:
|
||||
|
||||
```logsql
|
||||
_time:5m | extract_regexp "(?P<ip>([0-9]+[.]){3}[0-9]+)" | stats by (ip:/16) count() rows | first 10 by (rows desc)
|
||||
_time:5m | extract_regexp "(?P<ip>([0-9]+[.]){3}[0-9]+)" | stats by (ip:/16) count() rows | last 10 by (rows)
|
||||
```
|
||||
|
||||
## How to calculate the number of logs per every value of the given field?
|
||||
@@ -407,12 +407,12 @@ _time:5m | uniq by (host, path)
|
||||
|
||||
## How to return last N logs for the given query?
|
||||
|
||||
Use [`first` pipe](https://docs.victoriametrics.com/victorialogs/logsql/#first-pipe). For example, the following query returns the last 10 logs with the `error`
|
||||
Use [`last` pipe](https://docs.victoriametrics.com/victorialogs/logsql/#last-pipe). For example, the following query returns the last 10 logs with the `error`
|
||||
[word](https://docs.victoriametrics.com/victorialogs/logsql/#word) in the [`_msg` field](https://docs.victoriametrics.com/victorialogs/keyconcepts/#message-field)
|
||||
over the logs for the last 5 minutes:
|
||||
|
||||
```logsql
|
||||
_time:5m error | first 10 by (_time desc)
|
||||
_time:5m error | last 10 by (_time)
|
||||
```
|
||||
|
||||
It sorts the matching logs by [`_time` field](https://docs.victoriametrics.com/victorialogs/keyconcepts/#time-field) and then selects
|
||||
|
||||
@@ -18,16 +18,22 @@ It has the following features:
|
||||
- It supports live tailing - see [these docs](#live-tailing).
|
||||
|
||||
This tool can be obtained from the linked release pages at the [changelog](https://docs.victoriametrics.com/victorialogs/changelog/)
|
||||
or from [docker images](https://hub.docker.com/r/victoriametrics/vlogscli/tags).
|
||||
or from [docker images](https://hub.docker.com/r/victoriametrics/vlogscli/tags):
|
||||
|
||||
### Running `vlogscli` from release binary
|
||||
|
||||
```sh
|
||||
curl -L -O https://github.com/VictoriaMetrics/VictoriaMetrics/releases/download/v1.6.0-victorialogs/vlogscli-linux-amd64-v1.6.0-victorialogs.tar.gz
|
||||
tar xzf vlogscli-linux-amd64-v1.6.0-victorialogs.tar.gz
|
||||
curl -L -O https://github.com/VictoriaMetrics/VictoriaMetrics/releases/download/v1.4.0-victorialogs/vlogscli-linux-amd64-v1.4.0-victorialogs.tar.gz
|
||||
tar xzf vlogscli-linux-amd64-v1.4.0-victorialogs.tar.gz
|
||||
./vlogscli-prod
|
||||
```
|
||||
|
||||
### Running `vlogscli` from Docker image
|
||||
|
||||
```sh
|
||||
docker run --rm -it docker.io/victoriametrics/vlogscli:v1.4.0-victorialogs
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
By default `vlogscli` sends queries to [`http://localhost:8429/select/logsql/query`](https://docs.victoriametrics.com/victorialogs/querying/#querying-logs).
|
||||
|
||||
@@ -75,7 +75,7 @@ The `push_frequency` parameter{{% available_from "v1.18.7" anomaly %}} (default
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`url`</span>
|
||||
`url`
|
||||
</td>
|
||||
<td></td>
|
||||
<td>
|
||||
@@ -86,7 +86,7 @@ Link where to push metrics to. Example: `"http://localhost:8480/"`
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`tenant_id`</span>
|
||||
`tenant_id`
|
||||
</td>
|
||||
<td></td>
|
||||
<td>
|
||||
@@ -97,7 +97,7 @@ Tenant ID for cluster version. Example: `"0:0"`
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`health_path`</span>
|
||||
`health_path`
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -111,7 +111,7 @@ Tenant ID for cluster version. Example: `"0:0"`
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`user`</span>
|
||||
`user`
|
||||
</td>
|
||||
<td></td>
|
||||
<td>BasicAuth username</td>
|
||||
@@ -119,7 +119,7 @@ Tenant ID for cluster version. Example: `"0:0"`
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`password`</span>
|
||||
`password`
|
||||
</td>
|
||||
<td></td>
|
||||
<td>BasicAuth password</td>
|
||||
@@ -127,7 +127,7 @@ Tenant ID for cluster version. Example: `"0:0"`
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`bearer_token`</span>
|
||||
`bearer_token`
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -140,7 +140,7 @@ Token is passed in the standard format with header: `Authorization: bearer {toke
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`bearer_token_file`</span>
|
||||
`bearer_token_file`
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -153,7 +153,7 @@ Path to a file, which contains token, that is passed in the standard format with
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`verify_tls`</span>
|
||||
`verify_tls`
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -168,11 +168,11 @@ If a path to a CA bundle file (like `ca.crt`), it will verify the certificate us
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`tls_cert_file`</span>
|
||||
`tls_cert_file`
|
||||
</td>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`path/to/cert.crt`</span>
|
||||
`path/to/cert.crt`
|
||||
</td>
|
||||
<td>
|
||||
Path to a file with the client certificate, i.e. `client.crt`{{% available_from "v1.16.3" anomaly %}}.
|
||||
@@ -181,7 +181,7 @@ Path to a file with the client certificate, i.e. `client.crt`{{% available_from
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`tls_key_file`</span>
|
||||
`tls_key_file`
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -194,7 +194,7 @@ Path to a file with the client certificate key, i.e. `client.key`{{% available_f
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`timeout`</span>
|
||||
`timeout`
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -205,7 +205,7 @@ Path to a file with the client certificate key, i.e. `client.key`{{% available_f
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`push_frequency`</span>
|
||||
`push_frequency`
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -216,7 +216,7 @@ Path to a file with the client certificate key, i.e. `client.key`{{% available_f
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`extra_labels`</span>
|
||||
`extra_labels`
|
||||
</td>
|
||||
<td></td>
|
||||
<td>Section for custom labels specified by user.</td>
|
||||
@@ -271,7 +271,7 @@ For detailed guidance on configuring mTLS parameters such as `verify_tls`, `tls_
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`vmanomaly_start_time_seconds`</span>
|
||||
`vmanomaly_start_time_seconds`
|
||||
</td>
|
||||
<td>Gauge</td>
|
||||
<td>vmanomaly start time in UNIX time</td>
|
||||
@@ -279,7 +279,7 @@ For detailed guidance on configuring mTLS parameters such as `verify_tls`, `tls_
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`vmanomaly_version_info`</span>
|
||||
`vmanomaly_version_info`
|
||||
</td>
|
||||
<td>Gauge</td>
|
||||
<td>vmanomaly version information, contained in `version` label{{% available_from "v1.17.2" anomaly %}}.</td>
|
||||
@@ -287,7 +287,7 @@ For detailed guidance on configuring mTLS parameters such as `verify_tls`, `tls_
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`vmanomaly_ui_version_info`</span>
|
||||
`vmanomaly_ui_version_info`
|
||||
</td>
|
||||
<td>Gauge</td>
|
||||
<td>vmanomaly UI version information, contained in `version` label{{% available_from "v1.17.2" anomaly %}}.</td>
|
||||
@@ -295,7 +295,7 @@ For detailed guidance on configuring mTLS parameters such as `verify_tls`, `tls_
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`vmanomaly_available_memory_bytes`</span>
|
||||
`vmanomaly_available_memory_bytes`
|
||||
</td>
|
||||
<td>Gauge</td>
|
||||
<td>Virtual memory size in bytes, available to the process{{% available_from "v1.18.4" anomaly %}}.</td>
|
||||
@@ -303,7 +303,7 @@ For detailed guidance on configuring mTLS parameters such as `verify_tls`, `tls_
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`vmanomaly_cpu_cores_available`</span>
|
||||
`vmanomaly_cpu_cores_available`
|
||||
</td>
|
||||
<td>Gauge</td>
|
||||
<td>Number of (logical) CPU cores available to the process{{% available_from "v1.18.4" anomaly %}}.</td>
|
||||
@@ -331,11 +331,9 @@ Label names [description](#labelnames)
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`vmanomaly_reader_request_duration_seconds`</span>
|
||||
`vmanomaly_reader_request_duration_seconds`
|
||||
</td>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`Histogram`</span> (was `Summary`{{% deprecated_from "v1.17.0" anomaly %}})</td>
|
||||
<td>`Histogram` (was `Summary`{{% deprecated_from "v1.17.0" anomaly %}})</td>
|
||||
<td>The total time (in seconds) taken by queries to VictoriaMetrics `url` for the `query_key` query within the specified scheduler `scheduler_alias`, in the `vmanomaly` service running in `preset` mode.</td>
|
||||
<td>
|
||||
|
||||
@@ -345,12 +343,9 @@ Label names [description](#labelnames)
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`vmanomaly_reader_responses`</span> (named `vmanomaly_reader_response_count`{{% deprecated_from "v1.17.0" anomaly %}})
|
||||
</td>
|
||||
<td>
|
||||
|
||||
`Counter`
|
||||
`vmanomaly_reader_responses` (named `vmanomaly_reader_response_count`{{% deprecated_from "v1.17.0" anomaly %}})
|
||||
</td>
|
||||
<td>`Counter`</td>
|
||||
<td>The count of responses received from VictoriaMetrics `url` for the `query_key` query, categorized by `code`, within the specified scheduler `scheduler_alias`, in the `vmanomaly` service running in `preset` mode.</td>
|
||||
<td>
|
||||
|
||||
@@ -360,27 +355,21 @@ Label names [description](#labelnames)
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`vmanomaly_reader_received_bytes`</span>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
`Counter`
|
||||
`vmanomaly_reader_received_bytes`
|
||||
</td>
|
||||
<td>`Counter`</td>
|
||||
<td>The total number of bytes received in responses for the `query_key` query within the specified scheduler `scheduler_alias`, in the `vmanomaly` service running in `preset` mode.</td>
|
||||
<td>
|
||||
|
||||
`url`, `query_key`, <span style="white-space: nowrap;">`scheduler_alias`</span>, `preset`
|
||||
`url`, `query_key`, `scheduler_alias`, `preset`
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`vmanomaly_reader_response_parsing_seconds`</span>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
`Histogram` (was `Summary`{{% deprecated_from "v1.17.0" anomaly %}})
|
||||
`vmanomaly_reader_response_parsing_seconds`
|
||||
</td>
|
||||
<td>`Histogram` (was `Summary`{{% deprecated_from "v1.17.0" anomaly %}}</td>
|
||||
<td>The total time (in seconds) taken for data parsing at each `step` (json, dataframe) for the `query_key` query within the specified scheduler `scheduler_alias`, in the `vmanomaly` service running in `preset` mode.</td>
|
||||
<td>
|
||||
|
||||
@@ -390,12 +379,9 @@ Label names [description](#labelnames)
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`vmanomaly_reader_timeseries_received`</span>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
`Counter`
|
||||
`vmanomaly_reader_timeseries_received`
|
||||
</td>
|
||||
<td>`Counter`</td>
|
||||
<td>The total number of timeseries received from VictoriaMetrics for the `query_key` query within the specified scheduler `scheduler_alias`, in the `vmanomaly` service running in `preset` mode.</td>
|
||||
<td>
|
||||
|
||||
@@ -405,12 +391,9 @@ Label names [description](#labelnames)
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`vmanomaly_reader_datapoints_received`</span>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
`Counter`
|
||||
`vmanomaly_reader_datapoints_received`
|
||||
</td>
|
||||
<td>`Counter`</td>
|
||||
<td>The total number of datapoints received from VictoriaMetrics for the `query_key` query within the specified scheduler `scheduler_alias`, in the `vmanomaly` service running in `preset` mode.</td>
|
||||
<td>
|
||||
|
||||
@@ -442,12 +425,9 @@ Label names [description](#labelnames)
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`vmanomaly_model_runs`</span>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
`Counter`
|
||||
`vmanomaly_model_runs`
|
||||
</td>
|
||||
<td>`Counter`</td>
|
||||
<td>How many successful `stage` (`fit`, `infer`, `fit_infer`) runs occurred for models of class `model_alias` based on results from the `query_key` query, within the specified scheduler `scheduler_alias`, in the `vmanomaly` service running in `preset` mode.</td>
|
||||
<td>
|
||||
|
||||
@@ -457,11 +437,9 @@ Label names [description](#labelnames)
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`vmanomaly_model_run_duration_seconds`</span>
|
||||
`vmanomaly_model_run_duration_seconds`
|
||||
</td>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`Histogram`</span> (was `Summary`{{% deprecated_from "v1.17.0" anomaly %}}) </td>
|
||||
<td>`Histogram` (was `Summary`{{% deprecated_from "v1.17.0" anomaly %}}) </td>
|
||||
<td>The total time (in seconds) taken by model invocations during the `stage` (`fit`, `infer`, `fit_infer`), based on the results of the `query_key` query, for models of class `model_alias`, within the specified scheduler `scheduler_alias`, in the `vmanomaly` service running in `preset` mode.</td>
|
||||
<td>
|
||||
|
||||
@@ -471,12 +449,9 @@ Label names [description](#labelnames)
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`vmanomaly_model_datapoints_accepted`</span>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
`Counter`
|
||||
`vmanomaly_model_datapoints_accepted`
|
||||
</td>
|
||||
<td>`Counter`</td>
|
||||
<td>The number of datapoints accepted (excluding NaN or Inf values) by models of class `model_alias` from the results of the `query_key` query during the `stage` (`infer`, `fit_infer`), within the specified scheduler `scheduler_alias`, in the `vmanomaly` service running in `preset` mode.</td>
|
||||
<td>
|
||||
|
||||
@@ -486,12 +461,9 @@ Label names [description](#labelnames)
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`vmanomaly_model_datapoints_produced`</span>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
`Counter`
|
||||
`vmanomaly_model_datapoints_produced`
|
||||
</td>
|
||||
<td>`Counter`</td>
|
||||
<td>The number of datapoints generated by models of class `model_alias` during the `stage` (`infer`, `fit_infer`) based on results from the `query_key` query, within the specified scheduler `scheduler_alias`, in the `vmanomaly` service running in `preset` mode.</td>
|
||||
<td>
|
||||
|
||||
@@ -501,12 +473,9 @@ Label names [description](#labelnames)
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`vmanomaly_models_active`</span>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
`Gauge`
|
||||
`vmanomaly_models_active`
|
||||
</td>
|
||||
<td>`Gauge`</td>
|
||||
<td>The number of model instances of class `model_alias` currently available for inference for the `query_key` query, within the specified scheduler `scheduler_alias`, in the `vmanomaly` service running in `preset` mode.</td>
|
||||
<td>
|
||||
|
||||
@@ -516,12 +485,9 @@ Label names [description](#labelnames)
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`vmanomaly_model_runs_skipped`</span>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
`Counter`
|
||||
`vmanomaly_model_runs_skipped`
|
||||
</td>
|
||||
<td>`Counter`</td>
|
||||
<td>The number of times model runs (of class `model_alias`) were skipped in expected situations (e.g., no data for fitting/inference, or no new data to infer on) during the `stage` (`fit`, `infer`, `fit_infer`), based on results from the `query_key` query, within the specified scheduler `scheduler_alias`, in the `vmanomaly` service running in `preset` mode.</td>
|
||||
<td>
|
||||
|
||||
@@ -531,15 +497,12 @@ Label names [description](#labelnames)
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`vmanomaly_model_run_errors`</span>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
`Counter`
|
||||
`vmanomaly_model_run_errors`
|
||||
</td>
|
||||
<td>`Counter`</td>
|
||||
<td>The number of times model runs (of class `model_alias`) failed due to internal service errors during the `stage` (`fit`, `infer`, `fit_infer`), based on results from the `query_key` query, within the specified scheduler `scheduler_alias`, in the `vmanomaly` service running in `preset` mode.</td>
|
||||
<td>
|
||||
`stage`, `query_key`, `model_alias`, <span style="white-space: nowrap;">`scheduler_alias`</span>, `preset`
|
||||
`stage`, `query_key`, `model_alias`, `scheduler_alias`, `preset`
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -565,28 +528,22 @@ Label names [description](#labelnames)
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`vmanomaly_writer_request_duration_seconds`</span>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
`Histogram` (was `Summary`{{% deprecated_from "v1.17.0" anomaly %}})
|
||||
`vmanomaly_writer_request_duration_seconds`
|
||||
</td>
|
||||
<td>`Histogram` (was `Summary`{{% deprecated_from "v1.17.0" anomaly %}})</td>
|
||||
<td>The total time (in seconds) taken by write requests to VictoriaMetrics `url` for the `query_key` query within the specified scheduler `scheduler_alias`, in the `vmanomaly` service running in `preset` mode.
|
||||
</td>
|
||||
<td>
|
||||
|
||||
`url`, `query_key`, <span style="white-space: nowrap;">`scheduler_alias`</span>, `preset`
|
||||
`url`, `query_key`, `scheduler_alias`, `preset`
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`vmanomaly_writer_responses`</span> (named `vmanomaly_reader_response_count`{{% deprecated_from "v1.17.0" anomaly %}})
|
||||
</td>
|
||||
<td>
|
||||
|
||||
`Counter`
|
||||
`vmanomaly_writer_responses` (named `vmanomaly_reader_response_count`{{% deprecated_from "v1.17.0" anomaly %}})
|
||||
</td>
|
||||
<td>`Counter`</td>
|
||||
<td>The count of response codes received from VictoriaMetrics `url` for the `query_key` query, categorized by `code`, within the specified scheduler `scheduler_alias`, in the `vmanomaly` service running in `preset` mode.
|
||||
</td>
|
||||
<td>
|
||||
@@ -597,12 +554,9 @@ Label names [description](#labelnames)
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`vmanomaly_writer_sent_bytes`</span>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
`Counter`
|
||||
`vmanomaly_writer_sent_bytes`
|
||||
</td>
|
||||
<td>`Counter`</td>
|
||||
<td>The total number of bytes sent to VictoriaMetrics `url` for the `query_key` query within the specified scheduler `scheduler_alias`, in the `vmanomaly` service running in `preset` mode.</td>
|
||||
<td>
|
||||
|
||||
@@ -612,11 +566,9 @@ Label names [description](#labelnames)
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`vmanomaly_writer_request_serialize_seconds`</span>
|
||||
`vmanomaly_writer_request_serialize_seconds`
|
||||
</td>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`Histogram`</span> (was `Summary`{{% deprecated_from "v1.17.0" anomaly %}})</td>
|
||||
<td>`Histogram` (was `Summary`{{% deprecated_from "v1.17.0" anomaly %}}</td>
|
||||
<td>The total time (in seconds) taken for serializing data for the `query_key` query within the specified scheduler `scheduler_alias`, in the `vmanomaly` service running in `preset` mode.</td>
|
||||
<td>
|
||||
|
||||
@@ -626,12 +578,9 @@ Label names [description](#labelnames)
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`vmanomaly_writer_datapoints_sent`</span>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
`Counter`
|
||||
`vmanomaly_writer_datapoints_sent`
|
||||
</td>
|
||||
<td>`Counter`</td>
|
||||
<td>The total number of datapoints sent to VictoriaMetrics for the `query_key` query within the specified scheduler `scheduler_alias`, in the `vmanomaly` service running in `preset` mode.</td>
|
||||
<td>
|
||||
|
||||
@@ -640,13 +589,9 @@ Label names [description](#labelnames)
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`vmanomaly_writer_timeseries_sent`</span>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
`Counter`
|
||||
`vmanomaly_writer_timeseries_sent`
|
||||
</td>
|
||||
<td>`Counter`</td>
|
||||
<td>The total number of timeseries sent to VictoriaMetrics for the `query_key` query within the specified scheduler `scheduler_alias`, in the `vmanomaly` service running in `preset` mode.</td>
|
||||
<td>
|
||||
|
||||
|
||||
@@ -105,10 +105,9 @@ reader:
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`class`</span>
|
||||
`class`
|
||||
</td>
|
||||
<td>
|
||||
|
||||
`reader.vm.VmReader` (or `vm`{{% available_from "v1.13.0" anomaly %}})
|
||||
</td>
|
||||
<td>
|
||||
@@ -118,7 +117,7 @@ Name of the class needed to enable reading from VictoriaMetrics or Prometheus. V
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`queries`</span>
|
||||
`queries`
|
||||
</td>
|
||||
<td>
|
||||
See [per-query config example](#per-query-config-example) above
|
||||
@@ -130,11 +129,10 @@ See [per-query config section](#per-query-parameters) above
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`datasource_url`</span>
|
||||
`datasource_url`
|
||||
</td>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`http://localhost:8481/`</span>
|
||||
`http://localhost:8481/`
|
||||
</td>
|
||||
<td>
|
||||
Datasource URL address
|
||||
@@ -143,7 +141,7 @@ Datasource URL address
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`tenant_id`</span>
|
||||
`tenant_id`
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -156,7 +154,7 @@ For VictoriaMetrics Cluster version only, tenants are identified by `accountID`
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`sampling_period`</span>
|
||||
`sampling_period`
|
||||
</td>
|
||||
<td>
|
||||
`1h`
|
||||
@@ -168,11 +166,10 @@ Frequency of the points returned. Will be converted to `/query_range?step=%s` pa
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`query_range_path`</span>
|
||||
`query_range_path`
|
||||
</td>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`/api/v1/query_range`</span>
|
||||
`/api/v1/query_range`
|
||||
</td>
|
||||
<td>
|
||||
Performs PromQL/MetricsQL range query
|
||||
@@ -181,7 +178,7 @@ Performs PromQL/MetricsQL range query
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`health_path`</span>
|
||||
`health_path`
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -194,7 +191,7 @@ Absolute or relative URL address where to check availability of the datasource.
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`user`</span>
|
||||
`user`
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -207,7 +204,7 @@ BasicAuth username
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`password`</span>
|
||||
`password`
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -220,7 +217,7 @@ BasicAuth password
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`timeout`</span>
|
||||
`timeout`
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -233,7 +230,7 @@ Timeout for the requests, passed as a string
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`verify_tls`</span>
|
||||
`verify_tls`
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -248,7 +245,7 @@ If a path to a CA bundle file (like `ca.crt`), it will verify the certificate us
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`tls_cert_file`</span>
|
||||
`tls_cert_file`
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -261,7 +258,7 @@ Path to a file with the client certificate, i.e. `client.crt`{{% available_from
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`tls_key_file`</span>
|
||||
`tls_key_file`
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -274,7 +271,7 @@ Path to a file with the client certificate key, i.e. `client.key`{{% available_f
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`bearer_token`</span>
|
||||
`bearer_token`
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -287,7 +284,7 @@ Token is passed in the standard format with header: `Authorization: bearer {toke
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`bearer_token_file`</span>
|
||||
`bearer_token_file`
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -300,7 +297,7 @@ Path to a file, which contains token, that is passed in the standard format with
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`extra_filters`</span>
|
||||
`extra_filters`
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -313,7 +310,7 @@ List of strings with series selector. See: [Prometheus querying API enhancements
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`query_from_last_seen_timestamp`</span>
|
||||
`query_from_last_seen_timestamp`
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -326,7 +323,7 @@ If True, then query will be performed from the last seen timestamp for a given s
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`latency_offset`</span>
|
||||
`latency_offset`
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -339,7 +336,7 @@ It allows overriding the default `-search.latencyOffset`{{% available_from "v1.1
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`max_points_per_query`</span>
|
||||
`max_points_per_query`
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -352,7 +349,7 @@ Optional arg{{% available_from "v1.17.0" anomaly %}} overrides how `search.maxPo
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`tz`</span>
|
||||
`tz`
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -365,7 +362,7 @@ Optional argument{{% available_from "v1.18.0" anomaly %}} specifies the [IANA](h
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`data_range`</span>
|
||||
`data_range`
|
||||
</td>
|
||||
<td>
|
||||
|
||||
|
||||
@@ -116,7 +116,7 @@ Examples: `"50s"`, `"4m"`, `"3h"`, `"2d"`, `"1w"`.
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`fit_window`</span>
|
||||
`fit_window`
|
||||
</td>
|
||||
<td>str</td>
|
||||
<td>
|
||||
@@ -128,7 +128,7 @@ Examples: `"50s"`, `"4m"`, `"3h"`, `"2d"`, `"1w"`.
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`infer_every`</span>
|
||||
`infer_every`
|
||||
</td>
|
||||
<td>str</td>
|
||||
<td>
|
||||
@@ -140,7 +140,7 @@ Examples: `"50s"`, `"4m"`, `"3h"`, `"2d"`, `"1w"`.
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`fit_every`</span>
|
||||
`fit_every`
|
||||
</td>
|
||||
<td>str, Optional</td>
|
||||
<td>
|
||||
@@ -155,12 +155,12 @@ How often to completely retrain the models. If not set, value of `infer_every` i
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`start_from`{{% available_from "v1.18.5" anomaly %}}</span>
|
||||
`start_from`{{% available_from "v1.18.5" anomaly %}}
|
||||
</td>
|
||||
<td>str, <span style="white-space: nowrap;">Optional</span></td>
|
||||
<td>str, Optional</td>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`2024-11-26T01:00:00Z`</span>, `01:00`
|
||||
`2024-11-26T01:00:00Z`, `01:00`
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -170,9 +170,9 @@ Specifies when to initiate the first `fit_every` call. Accepts either an ISO 860
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`tz`{{% available_from "v1.18.5" anomaly %}}</span>
|
||||
`tz`{{% available_from "v1.18.5" anomaly %}}
|
||||
</td>
|
||||
<td>str, <span style="white-space: nowrap;">Optional</span></td>
|
||||
<td>str, Optional</td>
|
||||
<td>
|
||||
|
||||
`America/New_York`
|
||||
@@ -229,7 +229,7 @@ If a time zone is omitted, a timezone-naive datetime is used.
|
||||
<td>ISO 8601</td>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`fit_start_iso`</span>
|
||||
`fit_start_iso`
|
||||
</td>
|
||||
<td>str</td>
|
||||
<td>
|
||||
@@ -242,19 +242,16 @@ If a time zone is omitted, a timezone-naive datetime is used.
|
||||
<td>UNIX time</td>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`fit_start_s`</span>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">float</span>
|
||||
`fit_start_s`
|
||||
</td>
|
||||
<td>float</td>
|
||||
<td>1648771200</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>ISO 8601</td>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`fit_end_iso`</span>
|
||||
`fit_end_iso`
|
||||
</td>
|
||||
<td>str</td>
|
||||
<td>
|
||||
@@ -270,7 +267,7 @@ If a time zone is omitted, a timezone-naive datetime is used.
|
||||
<td>UNIX time</td>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`fit_end_s`</span>
|
||||
`fit_end_s`
|
||||
</td>
|
||||
<td>float</td>
|
||||
<td>1649548800</td>
|
||||
@@ -294,7 +291,7 @@ If a time zone is omitted, a timezone-naive datetime is used.
|
||||
<td>ISO 8601</td>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`infer_start_iso`</span>
|
||||
`infer_start_iso`
|
||||
</td>
|
||||
<td>str</td>
|
||||
<td>
|
||||
@@ -307,19 +304,16 @@ If a time zone is omitted, a timezone-naive datetime is used.
|
||||
<td>UNIX time</td>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`infer_start_s`</span>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">float</span>
|
||||
`infer_start_s`
|
||||
</td>
|
||||
<td>float</td>
|
||||
<td>1649635200</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>ISO 8601</td>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`infer_end_iso`</span>
|
||||
`infer_end_iso`
|
||||
</td>
|
||||
<td>str</td>
|
||||
<td>
|
||||
@@ -335,7 +329,7 @@ If a time zone is omitted, a timezone-naive datetime is used.
|
||||
<td>UNIX time</td>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`infer_end_s`</span>
|
||||
`infer_end_s`
|
||||
</td>
|
||||
<td>float</td>
|
||||
<td>1649894400</td>
|
||||
@@ -384,18 +378,9 @@ If a time zone is omitted, a timezone-naive datetime is used.
|
||||
<table class="params">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
|
||||
<span style="white-space: nowrap;">Parameter</span>
|
||||
</th>
|
||||
<th>
|
||||
|
||||
<span style="white-space: nowrap;">Type</span>
|
||||
</th>
|
||||
<th>
|
||||
|
||||
<span style="white-space: nowrap;">Example</span>
|
||||
</th>
|
||||
<th>Parameter</th>
|
||||
<th>Type</th>
|
||||
<th>Example</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -403,7 +388,7 @@ If a time zone is omitted, a timezone-naive datetime is used.
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`n_jobs`</span>
|
||||
`n_jobs`
|
||||
</td>
|
||||
<td>int</td>
|
||||
<td>
|
||||
@@ -424,22 +409,10 @@ This timeframe will be used for slicing on intervals `(fit_window, infer_window
|
||||
<table class="params">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
|
||||
<span style="white-space: nowrap;">Format</span>
|
||||
</th>
|
||||
<th>
|
||||
|
||||
<span style="white-space: nowrap;">Parameter</span>
|
||||
</th>
|
||||
<th>
|
||||
|
||||
<span style="white-space: nowrap;">Type</span>
|
||||
</th>
|
||||
<th>
|
||||
|
||||
<span style="white-space: nowrap;">Example</span>
|
||||
</th>
|
||||
<th>Format</th>
|
||||
<th>Parameter</th>
|
||||
<th>Type</th>
|
||||
<th>Example</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -448,7 +421,7 @@ This timeframe will be used for slicing on intervals `(fit_window, infer_window
|
||||
<td>ISO 8601</td>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`from_iso`</span>
|
||||
`from_iso`
|
||||
</td>
|
||||
<td>str</td>
|
||||
<td>
|
||||
@@ -461,7 +434,7 @@ This timeframe will be used for slicing on intervals `(fit_window, infer_window
|
||||
<td>UNIX time</td>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`from_s`</span>
|
||||
`from_s`
|
||||
</td>
|
||||
<td>float</td>
|
||||
<td>1648771200</td>
|
||||
@@ -470,7 +443,7 @@ This timeframe will be used for slicing on intervals `(fit_window, infer_window
|
||||
<td>ISO 8601</td>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`to_iso`</span>
|
||||
`to_iso`
|
||||
</td>
|
||||
<td>str</td>
|
||||
<td>
|
||||
@@ -486,7 +459,7 @@ This timeframe will be used for slicing on intervals `(fit_window, infer_window
|
||||
<td>UNIX time</td>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`to_s`</span>
|
||||
`to_s`
|
||||
</td>
|
||||
<td>float</td>
|
||||
<td>1649548800</td>
|
||||
@@ -511,7 +484,7 @@ The same *explicit* logic as in [Periodic scheduler](#periodic-scheduler)
|
||||
<td>ISO 8601</td>
|
||||
<td rowspan=2>
|
||||
|
||||
<span style="white-space: nowrap;">`fit_window`</span>
|
||||
`fit_window`
|
||||
</td>
|
||||
<td rowspan=2>str</td>
|
||||
<td>
|
||||
@@ -547,7 +520,7 @@ In `BacktestingScheduler`, the inference window is *implicitly* defined as a per
|
||||
<td>ISO 8601</td>
|
||||
<td rowspan=2>
|
||||
|
||||
<span style="white-space: nowrap;">`fit_every`</span>
|
||||
`fit_every`
|
||||
</td>
|
||||
<td rowspan=2>str</td>
|
||||
<td>
|
||||
|
||||
@@ -28,12 +28,11 @@ Future updates will introduce additional export methods, offering users more fle
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`class`</span>
|
||||
`class`
|
||||
</td>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`writer.vm.VmWriter` or `vm`{{% available_from "v1.13.0" anomaly %}}
|
||||
</span>
|
||||
`writer.vm.VmWriter` or `vm`{{% available_from "v1.13.0" anomaly %}}
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -43,11 +42,11 @@ Name of the class needed to enable writing to VictoriaMetrics or Prometheus. VmW
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`datasource_url`</span>
|
||||
`datasource_url`
|
||||
</td>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`http://localhost:8481/`</span>
|
||||
`http://localhost:8481/`
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -57,13 +56,11 @@ Datasource URL address
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`tenant_id`</span>
|
||||
`tenant_id`
|
||||
</td>
|
||||
<td>
|
||||
<span>
|
||||
|
||||
`0:0`, `multitenant`{{% available_from "v1.16.2" anomaly %}}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -74,11 +71,11 @@ For VictoriaMetrics Cluster version only, tenants are identified by `accountID`
|
||||
<tr>
|
||||
<td rowspan="4">
|
||||
|
||||
<span style="white-space: nowrap;">`metric_format`</span>
|
||||
`metric_format`
|
||||
</td>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`__name__: "vmanomaly_$VAR"`</span>
|
||||
`__name__: "vmanomaly_$VAR"`
|
||||
</td>
|
||||
<td rowspan="4">
|
||||
|
||||
@@ -100,26 +97,26 @@ Metrics to save the output (in metric names or labels). Must have `__name__` key
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`for: "$QUERY_KEY"`</span>
|
||||
`for: "$QUERY_KEY"`
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`run: "test_metric_format"`</span>
|
||||
`run: "test_metric_format"`
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`config: "io_vm_single.yaml"`</span>
|
||||
`config: "io_vm_single.yaml"`
|
||||
</td>
|
||||
</tr>
|
||||
<!-- End of additional rows -->
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`import_json_path`</span>
|
||||
`import_json_path`
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -133,7 +130,7 @@ Optional, to override the default import path
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`health_path`</span>
|
||||
`health_path`
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -147,7 +144,7 @@ Absolute or relative URL address where to check the availability of the datasour
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`user`</span>
|
||||
`user`
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -161,7 +158,7 @@ BasicAuth username
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`password`</span>
|
||||
`password`
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -175,7 +172,7 @@ BasicAuth password
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`timeout`</span>
|
||||
`timeout`
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -189,7 +186,7 @@ Timeout for the requests, passed as a string
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`verify_tls`</span>
|
||||
`verify_tls`
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -204,7 +201,7 @@ If a path to a CA bundle file (like `ca.crt`), it will verify the certificate us
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`tls_cert_file`</span>
|
||||
`tls_cert_file`
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -217,7 +214,7 @@ Path to a file with the client certificate, i.e. `client.crt`{{% available_from
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`tls_key_file`</span>
|
||||
`tls_key_file`
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -230,7 +227,7 @@ Path to a file with the client certificate key, i.e. `client.key`{{% available_f
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`bearer_token`</span>
|
||||
`bearer_token`
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -243,16 +240,15 @@ Token is passed in the standard format with header: `Authorization: bearer {toke
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`bearer_token_file`</span>
|
||||
`bearer_token_file`
|
||||
</td>
|
||||
<td>
|
||||
|
||||
`path_to_file`
|
||||
</td>
|
||||
<td>
|
||||
<span>
|
||||
Path to a file, which contains token, that is passed in the standard format with header: `Authorization: bearer {token}`{{% available_from "v1.15.9" anomaly %}}
|
||||
</span> </td>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
- To use *vmanomaly*, part of the enterprise package, a license key is required. Obtain your key [here](https://victoriametrics.com/products/enterprise/trial/) for this tutorial or for enterprise use.
|
||||
- In the tutorial, we'll be using the following VictoriaMetrics components:
|
||||
- [VictoriaMetrics Single-Node](https://docs.victoriametrics.com/single-server-victoriametrics) (v1.109.0)
|
||||
- [vmalert](https://docs.victoriametrics.com/vmalert/) (v1.109.0)
|
||||
- [vmagent](https://docs.victoriametrics.com/vmagent/) (v1.109.0)
|
||||
- [VictoriaMetrics Single-Node](https://docs.victoriametrics.com/single-server-victoriametrics) (v1.108.1)
|
||||
- [vmalert](https://docs.victoriametrics.com/vmalert/) (v1.108.1)
|
||||
- [vmagent](https://docs.victoriametrics.com/vmagent/) (v1.108.1)
|
||||
- [Grafana](https://grafana.com/) (v.10.2.1)
|
||||
- [Docker](https://docs.docker.com/get-docker/) and [Docker Compose](https://docs.docker.com/compose/)
|
||||
- [Node exporter](https://github.com/prometheus/node_exporter#node-exporter) (v1.7.0) and [Alertmanager](https://prometheus.io/docs/alerting/latest/alertmanager/) (v0.27.0)
|
||||
@@ -313,7 +313,7 @@ Let's wrap it all up together into the `docker-compose.yml` file.
|
||||
services:
|
||||
vmagent:
|
||||
container_name: vmagent
|
||||
image: victoriametrics/vmagent:v1.109.0
|
||||
image: victoriametrics/vmagent:v1.108.1
|
||||
depends_on:
|
||||
- "victoriametrics"
|
||||
ports:
|
||||
@@ -330,7 +330,7 @@ services:
|
||||
|
||||
victoriametrics:
|
||||
container_name: victoriametrics
|
||||
image: victoriametrics/victoria-metrics:v1.109.0
|
||||
image: victoriametrics/victoria-metrics:v1.108.1
|
||||
ports:
|
||||
- 8428:8428
|
||||
volumes:
|
||||
@@ -363,7 +363,7 @@ services:
|
||||
|
||||
vmalert:
|
||||
container_name: vmalert
|
||||
image: victoriametrics/vmalert:v1.109.0
|
||||
image: victoriametrics/vmalert:v1.108.1
|
||||
depends_on:
|
||||
- "victoriametrics"
|
||||
ports:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,11 @@
|
||||
---
|
||||
weight: 7
|
||||
weight: 6
|
||||
title: Year 2020
|
||||
menu:
|
||||
docs:
|
||||
identifier: vm-changelog-2020
|
||||
parent: vm-changelog
|
||||
weight: 7
|
||||
weight: 6
|
||||
aliases:
|
||||
- /CHANGELOG_2020.html
|
||||
- /changelog_2020
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
---
|
||||
weight: 6
|
||||
weight: 5
|
||||
title: Year 2021
|
||||
menu:
|
||||
docs:
|
||||
identifier: vm-changelog-2021
|
||||
parent: vm-changelog
|
||||
weight: 6
|
||||
weight: 5
|
||||
aliases:
|
||||
- /CHANGELOG_2021.html
|
||||
- /changelog_2021
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
---
|
||||
weight: 5
|
||||
weight: 4
|
||||
title: Year 2022
|
||||
menu:
|
||||
docs:
|
||||
identifier: vm-changelog-2022
|
||||
parent: vm-changelog
|
||||
weight: 5
|
||||
weight: 4
|
||||
aliases:
|
||||
- /CHANGELOG_2022.html
|
||||
- /changelog_2022
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
---
|
||||
weight: 4
|
||||
weight: 3
|
||||
title: Year 2023
|
||||
menu:
|
||||
docs:
|
||||
identifier: vm-changelog-2023
|
||||
parent: vm-changelog
|
||||
weight: 4
|
||||
weight: 3
|
||||
aliases:
|
||||
- /CHANGELOG_2023.html
|
||||
- /changelog_2023
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -82,7 +82,7 @@ VictoriaMetrics Enterprise components are available in the following forms:
|
||||
It is allowed to run VictoriaMetrics Enterprise components in [cases listed here](#valid-cases-for-victoriametrics-enterprise).
|
||||
|
||||
Binary releases of VictoriaMetrics Enterprise are available [at the releases page](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/latest).
|
||||
Enterprise binaries and packages have `enterprise` suffix in their names. For example, `victoria-metrics-linux-amd64-v1.109.0-enterprise.tar.gz`.
|
||||
Enterprise binaries and packages have `enterprise` suffix in their names. For example, `victoria-metrics-linux-amd64-v1.108.1-enterprise.tar.gz`.
|
||||
|
||||
In order to run binary release of VictoriaMetrics Enterprise component, please download the `*-enterprise.tar.gz` archive for your OS and architecture
|
||||
from the [releases page](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/latest) and unpack it. Then run the unpacked binary.
|
||||
@@ -100,8 +100,8 @@ For example, the following command runs VictoriaMetrics Enterprise binary with t
|
||||
obtained at [this page](https://victoriametrics.com/products/enterprise/trial/):
|
||||
|
||||
```sh
|
||||
wget https://github.com/VictoriaMetrics/VictoriaMetrics/releases/download/v1.109.0/victoria-metrics-linux-amd64-v1.109.0-enterprise.tar.gz
|
||||
tar -xzf victoria-metrics-linux-amd64-v1.109.0-enterprise.tar.gz
|
||||
wget https://github.com/VictoriaMetrics/VictoriaMetrics/releases/download/v1.108.1/victoria-metrics-linux-amd64-v1.108.1-enterprise.tar.gz
|
||||
tar -xzf victoria-metrics-linux-amd64-v1.108.1-enterprise.tar.gz
|
||||
./victoria-metrics-prod -license=BASE64_ENCODED_LICENSE_KEY
|
||||
```
|
||||
|
||||
@@ -116,7 +116,7 @@ Alternatively, VictoriaMetrics Enterprise license can be stored in the file and
|
||||
It is allowed to run VictoriaMetrics Enterprise components in [cases listed here](#valid-cases-for-victoriametrics-enterprise).
|
||||
|
||||
Docker images for VictoriaMetrics Enterprise are available [at VictoriaMetrics DockerHub](https://hub.docker.com/u/victoriametrics).
|
||||
Enterprise docker images have `enterprise` suffix in their names. For example, `victoriametrics/victoria-metrics:v1.109.0-enterprise`.
|
||||
Enterprise docker images have `enterprise` suffix in their names. For example, `victoriametrics/victoria-metrics:v1.108.1-enterprise`.
|
||||
|
||||
In order to run Docker image of VictoriaMetrics Enterprise component, it is required to provide the license key via command-line
|
||||
flag as described [here](#binary-releases).
|
||||
@@ -126,13 +126,13 @@ Enterprise license key can be obtained at [this page](https://victoriametrics.co
|
||||
For example, the following command runs VictoriaMetrics Enterprise Docker image with the specified license key:
|
||||
|
||||
```sh
|
||||
docker run --name=victoria-metrics victoriametrics/victoria-metrics:v1.109.0-enterprise -license=BASE64_ENCODED_LICENSE_KEY
|
||||
docker run --name=victoria-metrics victoriametrics/victoria-metrics:v1.108.1-enterprise -license=BASE64_ENCODED_LICENSE_KEY
|
||||
```
|
||||
|
||||
Alternatively, the license code can be stored in the file and then referred via `-licenseFile` command-line flag:
|
||||
|
||||
```sh
|
||||
docker run --name=victoria-metrics -v /vm-license:/vm-license victoriametrics/victoria-metrics:v1.109.0-enterprise -licenseFile=/path/to/vm-license
|
||||
docker run --name=victoria-metrics -v /vm-license:/vm-license victoriametrics/victoria-metrics:v1.108.1-enterprise -licenseFile=/path/to/vm-license
|
||||
```
|
||||
|
||||
Example docker-compose configuration:
|
||||
@@ -141,7 +141,7 @@ version: "3.5"
|
||||
services:
|
||||
victoriametrics:
|
||||
container_name: victoriametrics
|
||||
image: victoriametrics/victoria-metrics:v1.109.0
|
||||
image: victoriametrics/victoria-metrics:v1.108.1
|
||||
ports:
|
||||
- 8428:8428
|
||||
volumes:
|
||||
@@ -173,7 +173,7 @@ is used to provide key in plain-text:
|
||||
```yaml
|
||||
server:
|
||||
image:
|
||||
tag: v1.109.0-enterprise
|
||||
tag: v1.108.1-enterprise
|
||||
|
||||
license:
|
||||
key: {BASE64_ENCODED_LICENSE_KEY}
|
||||
@@ -184,7 +184,7 @@ In order to provide key via existing secret, the following values file is used:
|
||||
```yaml
|
||||
server:
|
||||
image:
|
||||
tag: v1.109.0-enterprise
|
||||
tag: v1.108.1-enterprise
|
||||
|
||||
license:
|
||||
secret:
|
||||
@@ -233,7 +233,7 @@ spec:
|
||||
license:
|
||||
key: {BASE64_ENCODED_LICENSE_KEY}
|
||||
image:
|
||||
tag: v1.109.0-enterprise
|
||||
tag: v1.108.1-enterprise
|
||||
```
|
||||
|
||||
In order to provide key via existing secret, the following custom resource is used:
|
||||
@@ -250,7 +250,7 @@ spec:
|
||||
name: vm-license
|
||||
key: license
|
||||
image:
|
||||
tag: v1.109.0-enterprise
|
||||
tag: v1.108.1-enterprise
|
||||
```
|
||||
|
||||
Example secret with license key:
|
||||
|
||||
@@ -236,27 +236,27 @@ services:
|
||||
- grafana_data:/var/lib/grafana/
|
||||
|
||||
vmsingle:
|
||||
image: victoriametrics/victoria-metrics:v1.109.0
|
||||
image: victoriametrics/victoria-metrics:v1.108.1
|
||||
command:
|
||||
- -httpListenAddr=0.0.0.0:8429
|
||||
|
||||
vmstorage:
|
||||
image: victoriametrics/vmstorage:v1.109.0-cluster
|
||||
image: victoriametrics/vmstorage:v1.108.1-cluster
|
||||
|
||||
vminsert:
|
||||
image: victoriametrics/vminsert:v1.109.0-cluster
|
||||
image: victoriametrics/vminsert:v1.108.1-cluster
|
||||
command:
|
||||
- -storageNode=vmstorage:8400
|
||||
- -httpListenAddr=0.0.0.0:8480
|
||||
|
||||
vmselect:
|
||||
image: victoriametrics/vmselect:v1.109.0-cluster
|
||||
image: victoriametrics/vmselect:v1.108.1-cluster
|
||||
command:
|
||||
- -storageNode=vmstorage:8401
|
||||
- -httpListenAddr=0.0.0.0:8481
|
||||
|
||||
vmagent:
|
||||
image: victoriametrics/vmagent:v1.109.0
|
||||
image: victoriametrics/vmagent:v1.108.1
|
||||
volumes:
|
||||
- ./scrape.yaml:/etc/vmagent/config.yaml
|
||||
command:
|
||||
@@ -265,7 +265,7 @@ services:
|
||||
- -remoteWrite.url=http://vmsingle:8429/api/v1/write
|
||||
|
||||
vmgateway-cluster:
|
||||
image: victoriametrics/vmgateway:v1.109.0-enterprise
|
||||
image: victoriametrics/vmgateway:v1.108.1-enterprise
|
||||
ports:
|
||||
- 8431:8431
|
||||
volumes:
|
||||
@@ -281,7 +281,7 @@ services:
|
||||
- -auth.oidcDiscoveryEndpoints=http://keycloak:8080/realms/master/.well-known/openid-configuration
|
||||
|
||||
vmgateway-single:
|
||||
image: victoriametrics/vmgateway:v1.109.0-enterprise
|
||||
image: victoriametrics/vmgateway:v1.108.1-enterprise
|
||||
ports:
|
||||
- 8432:8431
|
||||
volumes:
|
||||
@@ -393,7 +393,7 @@ Once iDP configuration is done, vmagent configuration needs to be updated to use
|
||||
|
||||
```yaml
|
||||
vmagent:
|
||||
image: victoriametrics/vmagent:v1.109.0
|
||||
image: victoriametrics/vmagent:v1.108.1
|
||||
volumes:
|
||||
- ./scrape.yaml:/etc/vmagent/config.yaml
|
||||
- ./vmagent-client-secret:/etc/vmagent/oauth2-client-secret
|
||||
|
||||
@@ -2,14 +2,6 @@
|
||||
|
||||
- TODO
|
||||
|
||||
## 0.8.13
|
||||
|
||||
**Release date:** 14 Jan 2025
|
||||
|
||||
 
|
||||
|
||||
- victorialogs version: v1.4.0 -> v1.5.0
|
||||
|
||||
## 0.8.12
|
||||
|
||||
**Release date:** 06 Jan 2025
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
@@ -2,14 +2,6 @@
|
||||
|
||||
- TODO
|
||||
|
||||
## 0.15.4
|
||||
|
||||
**Release date:** 14 Jan 2025
|
||||
|
||||
 
|
||||
|
||||
- bump version of VM components to [v1.109.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.109.0)
|
||||
|
||||
## 0.15.3
|
||||
|
||||
**Release date:** 06 Jan 2025
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
@@ -2,14 +2,6 @@
|
||||
|
||||
- TODO
|
||||
|
||||
## 0.13.6
|
||||
|
||||
**Release date:** 14 Jan 2025
|
||||
|
||||
 
|
||||
|
||||
- bump version of VM components to [v1.109.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.109.0)
|
||||
|
||||
## 0.13.5
|
||||
|
||||
**Release date:** 06 Jan 2025
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
@@ -2,14 +2,6 @@
|
||||
|
||||
- TODO
|
||||
|
||||
## 0.8.4
|
||||
|
||||
**Release date:** 14 Jan 2025
|
||||
|
||||
 
|
||||
|
||||
- bump version of VM components to [v1.109.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.109.0)
|
||||
|
||||
## 0.8.3
|
||||
|
||||
**Release date:** 06 Jan 2025
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
@@ -2,14 +2,6 @@
|
||||
|
||||
- TODO
|
||||
|
||||
## 0.17.1
|
||||
|
||||
**Release date:** 14 Jan 2025
|
||||
|
||||
 
|
||||
|
||||
- bump version of VM components to [v1.109.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.109.0)
|
||||
|
||||
## 0.17.0
|
||||
|
||||
**Release date:** 09 Jan 2025
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
@@ -1,24 +1,6 @@
|
||||
## Next release
|
||||
|
||||
- TODO
|
||||
|
||||
## 0.7.2
|
||||
|
||||
**Release date:** 14 Jan 2025
|
||||
|
||||
 
|
||||
|
||||
- bump version of VM components to [v1.109.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.109.0)
|
||||
|
||||
## 0.7.1
|
||||
|
||||
**Release date:** 10 Jan 2025
|
||||
|
||||
 
|
||||
|
||||
- updated common dependency 0.0.35 -> 0.0.37
|
||||
- fixed typo useMultitenantMode -> useMultiTenantMode in remotewrite settings
|
||||
- allow passing additional remotewrite setings
|
||||
|
||||
## 0.7.0
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
@@ -2,14 +2,6 @@
|
||||
|
||||
- TODO
|
||||
|
||||
## 0.6.4
|
||||
|
||||
**Release date:** 14 Jan 2025
|
||||
|
||||
 
|
||||
|
||||
- bump version of VM components to [v1.109.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.109.0)
|
||||
|
||||
## 0.6.3
|
||||
|
||||
**Release date:** 06 Jan 2025
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
@@ -2,22 +2,6 @@
|
||||
|
||||
- TODO
|
||||
|
||||
## 0.33.4
|
||||
|
||||
**Release date:** 14 Jan 2025
|
||||
|
||||
 
|
||||
|
||||
- bump version of VM components to [v1.109.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.109.0)
|
||||
|
||||
## 0.33.3
|
||||
|
||||
**Release date:** 13 Jan 2025
|
||||
|
||||
 
|
||||
|
||||
- updates operator to [v0.51.3](https://github.com/VictoriaMetrics/operator/releases/tag/v0.51.3) version
|
||||
|
||||
## 0.33.2
|
||||
|
||||
**Release date:** 06 Jan 2025
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
@@ -2,14 +2,6 @@
|
||||
|
||||
- TODO
|
||||
|
||||
## 0.40.4
|
||||
|
||||
**Release date:** 13 Jan 2025
|
||||
|
||||
 
|
||||
|
||||
- updates operator to [v0.51.3](https://github.com/VictoriaMetrics/operator/releases/tag/v0.51.3) version
|
||||
|
||||
## 0.40.3
|
||||
|
||||
**Release date:** 06 Jan 2025
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
@@ -2,14 +2,6 @@
|
||||
|
||||
- TODO
|
||||
|
||||
## 0.13.5
|
||||
|
||||
**Release date:** 14 Jan 2025
|
||||
|
||||
 
|
||||
|
||||
- bump version of VM components to [v1.109.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.109.0)
|
||||
|
||||
## 0.13.4
|
||||
|
||||
**Release date:** 06 Jan 2025
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
@@ -30,8 +30,8 @@ scrape_configs:
|
||||
After you created the `scrape.yaml` file, download and unpack [single-node VictoriaMetrics](https://docs.victoriametrics.com/) to the same directory:
|
||||
|
||||
```
|
||||
wget https://github.com/VictoriaMetrics/VictoriaMetrics/releases/download/v1.109.0/victoria-metrics-linux-amd64-v1.109.0.tar.gz
|
||||
tar xzf victoria-metrics-linux-amd64-v1.109.0.tar.gz
|
||||
wget https://github.com/VictoriaMetrics/VictoriaMetrics/releases/download/v1.108.1/victoria-metrics-linux-amd64-v1.108.1.tar.gz
|
||||
tar xzf victoria-metrics-linux-amd64-v1.108.1.tar.gz
|
||||
```
|
||||
|
||||
Then start VictoriaMetrics and instruct it to scrape targets defined in `scrape.yaml` and save scraped metrics
|
||||
@@ -146,8 +146,8 @@ Then start [single-node VictoriaMetrics](https://docs.victoriametrics.com/) acco
|
||||
|
||||
```yaml
|
||||
# Download and unpack single-node VictoriaMetrics
|
||||
wget https://github.com/VictoriaMetrics/VictoriaMetrics/releases/download/v1.109.0/victoria-metrics-linux-amd64-v1.109.0.tar.gz
|
||||
tar xzf victoria-metrics-linux-amd64-v1.109.0.tar.gz
|
||||
wget https://github.com/VictoriaMetrics/VictoriaMetrics/releases/download/v1.108.1/victoria-metrics-linux-amd64-v1.108.1.tar.gz
|
||||
tar xzf victoria-metrics-linux-amd64-v1.108.1.tar.gz
|
||||
|
||||
# Run single-node VictoriaMetrics with the given scrape.yaml
|
||||
./victoria-metrics-prod -promscrape.config=scrape.yaml
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
page: search
|
||||
layout: search
|
||||
draft: false
|
||||
weight: 0
|
||||
search: true
|
||||
---
|
||||
@@ -149,7 +149,7 @@ or [Prometheus recording rules definition format](https://prometheus.io/docs/pro
|
||||
|
||||
There are limitations for the rules files:
|
||||
|
||||
1. All files may contain no more than 100 rules in total. If you need to upload more rules contact us via [support-cloud@victoriametrics.com](mailto:support-cloud@victoriametrics.com).
|
||||
1. All files may contain no more than 100 rules in total. If you need to upload more rules contact us via [support@victoriametrics.com](mailto:support@victoriametrics.com).
|
||||
2. The maximum file size is 20mb.
|
||||
3. The names of the groups in the files should be unique.
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 24 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 28 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
@@ -18,7 +18,7 @@ The tier parameters are derived from testing in typical monitoring environments,
|
||||
| **Active Time Series Count** | Per Tier Limits | Number of [active time series](https://docs.victoriametrics.com/faq/#what-is-an-active-time-series) that received at least one data point in the last hour. |
|
||||
| **Read Rate** | Per Tier Limits | Number of datapoints retrieved from the database per second. |
|
||||
| **New Series Over 24 Hours** (churn rate) | `<= Active Time Series Count` | Number of new series created in 24 hours. High [churn rate](https://docs.victoriametrics.com/faq/#what-is-high-churn-rate) leads to higher resource consumption. |
|
||||
| **Concurrent Requests per Token** | `<= 600` | Maximum concurrent requests per access token. It is recommended to create separate tokens for different clients and environments. This can be adjusted via [support](mailto:support-cloud@victoriametrics.com). |
|
||||
| **Concurrent Requests per Token** | `<= 600` | Maximum concurrent requests per access token. It is recommended to create separate tokens for different clients and environments. This can be adjusted via [support](mailto:support@victoriametrics.com). |
|
||||
|
||||
For a detailed explanation of each parameter, visit the guide on [Understanding Your Setup Size](https://docs.victoriametrics.com/guides/understand-your-setup-size.html).
|
||||
|
||||
@@ -26,7 +26,7 @@ For a detailed explanation of each parameter, visit the guide on [Understanding
|
||||
|
||||
| **Flag** | **Default Value** | **Description** |
|
||||
|-----------------------------------|---------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| **Max Label Value Length** | `<= 1kb` (Default: `4kb`) | Maximum length of label values. Longer values are truncated. Large label values can lead to high RAM consumption. This can be adjusted via [support](mailto:support-cloud@victoriametrics.com). |
|
||||
| **Max Label Value Length** | `<= 1kb` (Default: `4kb`) | Maximum length of label values. Longer values are truncated. Large label values can lead to high RAM consumption. This can be adjusted via [support](mailto:support@victoriametrics.com). |
|
||||
| **Max Labels per Time Series** | `<= 30` | Maximum number of labels per time series. Excess labels are dropped. Higher values can increase [cardinality](https://docs.victoriametrics.com/keyconcepts/#cardinality) and resource usage. This can be configured in [deployment settings](https://docs.victoriametrics.com/victoriametrics-cloud/quickstart/#modifying-an-existing-deployment). |
|
||||
|
||||
|
||||
|
||||
@@ -273,6 +273,7 @@ expr: <string>
|
||||
|
||||
# Labels to add or overwrite for each alert.
|
||||
# In case of conflicts, original labels are kept with prefix `exported_`.
|
||||
# label value supports [templating functions](#template-functions), [reusable templates](#reusable-templates) and limited variables, such as {{$value}}, {{$expr}}.
|
||||
labels:
|
||||
[ <labelname>: <tmpl_string> ]
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
"unsafe"
|
||||
@@ -228,16 +227,6 @@ func (br *blockResult) cloneValues(values []string) []string {
|
||||
return br.valuesBuf[valuesBufLen:]
|
||||
}
|
||||
|
||||
func (br *blockResult) addValues(values []string) {
|
||||
valuesBufLen := len(br.valuesBuf)
|
||||
br.valuesBuf = slicesutil.SetLength(br.valuesBuf, valuesBufLen+len(values))
|
||||
valuesBuf := br.valuesBuf[valuesBufLen:]
|
||||
_ = valuesBuf[len(values)-1]
|
||||
for i, v := range values {
|
||||
valuesBuf[i] = br.a.copyString(v)
|
||||
}
|
||||
}
|
||||
|
||||
func (br *blockResult) addValue(v string) {
|
||||
valuesBuf := br.valuesBuf
|
||||
if len(valuesBuf) > 0 && v == valuesBuf[len(valuesBuf)-1] {
|
||||
@@ -292,14 +281,11 @@ func (br *blockResult) addResultColumn(rc *resultColumn) {
|
||||
logger.Panicf("BUG: column %q must contain %d rows, but it contains %d rows", rc.name, br.rowsLen, len(rc.values))
|
||||
}
|
||||
if areConstValues(rc.values) {
|
||||
// Clone the constant value into rc, so it doesn't hold the external memory.
|
||||
// This optimization allows reducing memory usage after br cloning.
|
||||
br.addValue(rc.values[0])
|
||||
valuesEncoded := br.valuesBuf[len(br.valuesBuf)-1:]
|
||||
// This optimization allows reducing memory usage after br cloning
|
||||
br.csAdd(blockResultColumn{
|
||||
name: rc.name,
|
||||
isConst: true,
|
||||
valuesEncoded: valuesEncoded,
|
||||
valuesEncoded: rc.values[:1],
|
||||
})
|
||||
} else {
|
||||
br.csAdd(blockResultColumn{
|
||||
@@ -504,57 +490,73 @@ func (br *blockResult) newValuesEncodedFromColumnHeader(bs *blockSearch, bm *bit
|
||||
|
||||
switch ch.valueType {
|
||||
case valueTypeString:
|
||||
visitValuesReadonly(bs, ch, bm, br.addValues)
|
||||
visitValuesReadonly(bs, ch, bm, br.addValue)
|
||||
case valueTypeDict:
|
||||
visitValuesReadonly(bs, ch, bm, func(values []string) {
|
||||
checkValuesSize(bs, ch, values, 1, "dict")
|
||||
for _, v := range values {
|
||||
dictIdx := v[0]
|
||||
if int(dictIdx) >= len(ch.valuesDict.values) {
|
||||
logger.Panicf("FATAL: %s: too big dict index for column %q: %d; should be smaller than %d", bs.partPath(), ch.name, dictIdx, len(ch.valuesDict.values))
|
||||
}
|
||||
visitValuesReadonly(bs, ch, bm, func(v string) {
|
||||
if len(v) != 1 {
|
||||
logger.Panicf("FATAL: %s: unexpected dict value size for column %q; got %d bytes; want 1 byte", bs.partPath(), ch.name, len(v))
|
||||
}
|
||||
br.addValues(values)
|
||||
dictIdx := v[0]
|
||||
if int(dictIdx) >= len(ch.valuesDict.values) {
|
||||
logger.Panicf("FATAL: %s: too big dict index for column %q: %d; should be smaller than %d", bs.partPath(), ch.name, dictIdx, len(ch.valuesDict.values))
|
||||
}
|
||||
br.addValue(v)
|
||||
})
|
||||
case valueTypeUint8:
|
||||
visitValuesReadonly(bs, ch, bm, func(values []string) {
|
||||
checkValuesSize(bs, ch, values, 1, "uint8")
|
||||
br.addValues(values)
|
||||
visitValuesReadonly(bs, ch, bm, func(v string) {
|
||||
if len(v) != 1 {
|
||||
logger.Panicf("FATAL: %s: unexpected size for uint8 column %q; got %d bytes; want 1 byte", bs.partPath(), ch.name, len(v))
|
||||
}
|
||||
br.addValue(v)
|
||||
})
|
||||
case valueTypeUint16:
|
||||
visitValuesReadonly(bs, ch, bm, func(values []string) {
|
||||
checkValuesSize(bs, ch, values, 2, "uint16")
|
||||
br.addValues(values)
|
||||
visitValuesReadonly(bs, ch, bm, func(v string) {
|
||||
if len(v) != 2 {
|
||||
logger.Panicf("FATAL: %s: unexpected size for uint16 column %q; got %d bytes; want 2 bytes", bs.partPath(), ch.name, len(v))
|
||||
}
|
||||
br.addValue(v)
|
||||
})
|
||||
case valueTypeUint32:
|
||||
visitValuesReadonly(bs, ch, bm, func(values []string) {
|
||||
checkValuesSize(bs, ch, values, 4, "uint32")
|
||||
br.addValues(values)
|
||||
visitValuesReadonly(bs, ch, bm, func(v string) {
|
||||
if len(v) != 4 {
|
||||
logger.Panicf("FATAL: %s: unexpected size for uint32 column %q; got %d bytes; want 4 bytes", bs.partPath(), ch.name, len(v))
|
||||
}
|
||||
br.addValue(v)
|
||||
})
|
||||
case valueTypeUint64:
|
||||
visitValuesReadonly(bs, ch, bm, func(values []string) {
|
||||
checkValuesSize(bs, ch, values, 8, "uint64")
|
||||
br.addValues(values)
|
||||
visitValuesReadonly(bs, ch, bm, func(v string) {
|
||||
if len(v) != 8 {
|
||||
logger.Panicf("FATAL: %s: unexpected size for uint64 column %q; got %d bytes; want 8 bytes", bs.partPath(), ch.name, len(v))
|
||||
}
|
||||
br.addValue(v)
|
||||
})
|
||||
case valueTypeInt64:
|
||||
visitValuesReadonly(bs, ch, bm, func(values []string) {
|
||||
checkValuesSize(bs, ch, values, 8, "int64")
|
||||
br.addValues(values)
|
||||
visitValuesReadonly(bs, ch, bm, func(v string) {
|
||||
if len(v) != 8 {
|
||||
logger.Panicf("FATAL: %s: unexpected size for int64 column %q; got %d bytes; want 8 bytes", bs.partPath(), ch.name, len(v))
|
||||
}
|
||||
br.addValue(v)
|
||||
})
|
||||
case valueTypeFloat64:
|
||||
visitValuesReadonly(bs, ch, bm, func(values []string) {
|
||||
checkValuesSize(bs, ch, values, 8, "float64")
|
||||
br.addValues(values)
|
||||
visitValuesReadonly(bs, ch, bm, func(v string) {
|
||||
if len(v) != 8 {
|
||||
logger.Panicf("FATAL: %s: unexpected size for float64 column %q; got %d bytes; want 8 bytes", bs.partPath(), ch.name, len(v))
|
||||
}
|
||||
br.addValue(v)
|
||||
})
|
||||
case valueTypeIPv4:
|
||||
visitValuesReadonly(bs, ch, bm, func(values []string) {
|
||||
checkValuesSize(bs, ch, values, 4, "ipv4")
|
||||
br.addValues(values)
|
||||
visitValuesReadonly(bs, ch, bm, func(v string) {
|
||||
if len(v) != 4 {
|
||||
logger.Panicf("FATAL: %s: unexpected size for ipv4 column %q; got %d bytes; want 4 bytes", bs.partPath(), ch.name, len(v))
|
||||
}
|
||||
br.addValue(v)
|
||||
})
|
||||
case valueTypeTimestampISO8601:
|
||||
visitValuesReadonly(bs, ch, bm, func(values []string) {
|
||||
checkValuesSize(bs, ch, values, 8, "iso8601")
|
||||
br.addValues(values)
|
||||
visitValuesReadonly(bs, ch, bm, func(v string) {
|
||||
if len(v) != 8 {
|
||||
logger.Panicf("FATAL: %s: unexpected size for timestmap column %q; got %d bytes; want 8 bytes", bs.partPath(), ch.name, len(v))
|
||||
}
|
||||
br.addValue(v)
|
||||
})
|
||||
default:
|
||||
logger.Panicf("FATAL: %s: unknown valueType=%d for column %q", bs.partPath(), ch.valueType, ch.name)
|
||||
@@ -563,14 +565,6 @@ func (br *blockResult) newValuesEncodedFromColumnHeader(bs *blockSearch, bm *bit
|
||||
return br.valuesBuf[valuesBufLen:]
|
||||
}
|
||||
|
||||
func checkValuesSize(bs *blockSearch, ch *columnHeader, values []string, sizeExpected int, typeStr string) {
|
||||
for _, v := range values {
|
||||
if len(v) != sizeExpected {
|
||||
logger.Panicf("FATAL: %s: unexpected size for %s column %q; got %d bytes; want %d bytes", typeStr, bs.partPath(), ch.name, len(v), sizeExpected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// addColumn adds column for the given ch to br.
|
||||
//
|
||||
// The added column is valid until ch is changed.
|
||||
@@ -2144,46 +2138,17 @@ func getEmptyStrings(rowsLen int) []string {
|
||||
|
||||
var emptyStrings atomic.Pointer[[]string]
|
||||
|
||||
func visitValuesReadonly(bs *blockSearch, ch *columnHeader, bm *bitmap, f func(values []string)) {
|
||||
func visitValuesReadonly(bs *blockSearch, ch *columnHeader, bm *bitmap, f func(value string)) {
|
||||
if bm.isZero() {
|
||||
// Fast path - nothing to visit
|
||||
return
|
||||
}
|
||||
values := bs.getValuesForColumn(ch)
|
||||
if bm.areAllBitsSet() {
|
||||
// Faster path - visit all the values
|
||||
f(values)
|
||||
return
|
||||
}
|
||||
|
||||
// Slower path - visit only the selected values
|
||||
vb := getValuesBuf()
|
||||
bm.forEachSetBitReadonly(func(idx int) {
|
||||
vb.values = append(vb.values, values[idx])
|
||||
f(values[idx])
|
||||
})
|
||||
f(vb.values)
|
||||
putValuesBuf(vb)
|
||||
}
|
||||
|
||||
type valuesBuf struct {
|
||||
values []string
|
||||
}
|
||||
|
||||
func getValuesBuf() *valuesBuf {
|
||||
v := valuesBufPool.Get()
|
||||
if v == nil {
|
||||
return &valuesBuf{}
|
||||
}
|
||||
return v.(*valuesBuf)
|
||||
}
|
||||
|
||||
func putValuesBuf(vb *valuesBuf) {
|
||||
vb.values = vb.values[:0]
|
||||
valuesBufPool.Put(vb)
|
||||
}
|
||||
|
||||
var valuesBufPool sync.Pool
|
||||
|
||||
func getCanonicalColumnName(columnName string) string {
|
||||
if columnName == "" {
|
||||
return "_msg"
|
||||
|
||||
@@ -12,7 +12,6 @@ type chunkedAllocator struct {
|
||||
countEmptyProcessors []statsCountEmptyProcessor
|
||||
countUniqProcessors []statsCountUniqProcessor
|
||||
countUniqHashProcessors []statsCountUniqHashProcessor
|
||||
histogramProcessors []statsHistogramProcessor
|
||||
maxProcessors []statsMaxProcessor
|
||||
medianProcessors []statsMedianProcessor
|
||||
minProcessors []statsMinProcessor
|
||||
@@ -61,11 +60,6 @@ func (a *chunkedAllocator) newStatsCountUniqHashProcessor() (p *statsCountUniqHa
|
||||
return p
|
||||
}
|
||||
|
||||
func (a *chunkedAllocator) newStatsHistogramProcessor() (p *statsHistogramProcessor) {
|
||||
a.histogramProcessors, p = addNewItem(a.histogramProcessors, a)
|
||||
return p
|
||||
}
|
||||
|
||||
func (a *chunkedAllocator) newStatsMaxProcessor() (p *statsMaxProcessor) {
|
||||
a.maxProcessors, p = addNewItem(a.maxProcessors, a)
|
||||
return p
|
||||
|
||||
@@ -2,8 +2,6 @@ package logstorage
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fs"
|
||||
)
|
||||
|
||||
func TestMatchAnyCasePhrase(t *testing.T) {
|
||||
@@ -46,6 +44,8 @@ func TestFilterAnyCasePhrase(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("single-row", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -119,6 +119,8 @@ func TestFilterAnyCasePhrase(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("const-column", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "other-column",
|
||||
@@ -228,6 +230,8 @@ func TestFilterAnyCasePhrase(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("dict", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -283,6 +287,8 @@ func TestFilterAnyCasePhrase(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("strings", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -353,6 +359,8 @@ func TestFilterAnyCasePhrase(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("uint8", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -418,6 +426,8 @@ func TestFilterAnyCasePhrase(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("uint16", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -482,6 +492,8 @@ func TestFilterAnyCasePhrase(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("uint32", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -546,6 +558,8 @@ func TestFilterAnyCasePhrase(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("uint64", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -609,6 +623,8 @@ func TestFilterAnyCasePhrase(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("int64", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -672,6 +688,8 @@ func TestFilterAnyCasePhrase(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("float64", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -777,6 +795,8 @@ func TestFilterAnyCasePhrase(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("ipv4", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -873,6 +893,8 @@ func TestFilterAnyCasePhrase(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("timestamp-iso8601", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "_msg",
|
||||
@@ -954,7 +976,4 @@ func TestFilterAnyCasePhrase(t *testing.T) {
|
||||
}
|
||||
testFilterMatchForColumns(t, columns, pf, "_msg", nil)
|
||||
})
|
||||
|
||||
// Remove the remaining data files for the test
|
||||
fs.MustRemoveAll(t.Name())
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@ package logstorage
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fs"
|
||||
)
|
||||
|
||||
func TestMatchAnyCasePrefix(t *testing.T) {
|
||||
@@ -46,6 +44,8 @@ func TestFilterAnyCasePrefix(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("single-row", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -137,6 +137,8 @@ func TestFilterAnyCasePrefix(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("const-column", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "other-column",
|
||||
@@ -252,6 +254,8 @@ func TestFilterAnyCasePrefix(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("dict", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -307,6 +311,8 @@ func TestFilterAnyCasePrefix(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("strings", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -389,6 +395,8 @@ func TestFilterAnyCasePrefix(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("uint8", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -454,6 +462,8 @@ func TestFilterAnyCasePrefix(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("uint16", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -518,6 +528,8 @@ func TestFilterAnyCasePrefix(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("uint32", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -582,6 +594,8 @@ func TestFilterAnyCasePrefix(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("uint64", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -645,6 +659,8 @@ func TestFilterAnyCasePrefix(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("int64", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -708,6 +724,8 @@ func TestFilterAnyCasePrefix(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("float64", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -819,6 +837,8 @@ func TestFilterAnyCasePrefix(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("ipv4", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -915,6 +935,8 @@ func TestFilterAnyCasePrefix(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("timestamp-iso8601", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "_msg",
|
||||
@@ -996,7 +1018,4 @@ func TestFilterAnyCasePrefix(t *testing.T) {
|
||||
}
|
||||
testFilterMatchForColumns(t, columns, fp, "_msg", nil)
|
||||
})
|
||||
|
||||
// Remove the remaining data files for the test
|
||||
fs.MustRemoveAll(t.Name())
|
||||
}
|
||||
|
||||
@@ -2,14 +2,14 @@ package logstorage
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fs"
|
||||
)
|
||||
|
||||
func TestFilterExactPrefix(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("single-row", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -59,6 +59,8 @@ func TestFilterExactPrefix(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("const-column", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -110,6 +112,8 @@ func TestFilterExactPrefix(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("dict", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -153,6 +157,8 @@ func TestFilterExactPrefix(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("strings", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -205,6 +211,8 @@ func TestFilterExactPrefix(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("uint8", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -258,6 +266,8 @@ func TestFilterExactPrefix(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("uint16", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -311,6 +321,8 @@ func TestFilterExactPrefix(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("uint32", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -364,6 +376,8 @@ func TestFilterExactPrefix(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("uint64", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -417,6 +431,8 @@ func TestFilterExactPrefix(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("int64", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -482,6 +498,8 @@ func TestFilterExactPrefix(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("float64", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -539,6 +557,8 @@ func TestFilterExactPrefix(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("ipv4", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -587,6 +607,8 @@ func TestFilterExactPrefix(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("timestamp-iso8601", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "_msg",
|
||||
@@ -630,7 +652,4 @@ func TestFilterExactPrefix(t *testing.T) {
|
||||
}
|
||||
testFilterMatchForColumns(t, columns, fep, "_msg", nil)
|
||||
})
|
||||
|
||||
// Remove the remaining data files for the test
|
||||
fs.MustRemoveAll(t.Name())
|
||||
}
|
||||
|
||||
@@ -2,14 +2,14 @@ package logstorage
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fs"
|
||||
)
|
||||
|
||||
func TestFilterExact(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("single-row", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -47,6 +47,8 @@ func TestFilterExact(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("const-column", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -92,6 +94,8 @@ func TestFilterExact(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("dict", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -135,6 +139,8 @@ func TestFilterExact(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("strings", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -187,6 +193,8 @@ func TestFilterExact(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("uint8", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -240,6 +248,8 @@ func TestFilterExact(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("uint16", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -293,6 +303,8 @@ func TestFilterExact(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("uint32", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -346,6 +358,8 @@ func TestFilterExact(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("uint64", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -399,6 +413,8 @@ func TestFilterExact(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("int64", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -458,6 +474,8 @@ func TestFilterExact(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("float64", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -533,6 +551,8 @@ func TestFilterExact(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("ipv4", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -593,6 +613,8 @@ func TestFilterExact(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("timestamp-iso8601", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "_msg",
|
||||
@@ -642,7 +664,4 @@ func TestFilterExact(t *testing.T) {
|
||||
}
|
||||
testFilterMatchForColumns(t, columns, fe, "_msg", nil)
|
||||
})
|
||||
|
||||
// Remove the remaining data files for the test
|
||||
fs.MustRemoveAll(t.Name())
|
||||
}
|
||||
|
||||
@@ -4,14 +4,14 @@ import (
|
||||
"reflect"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fs"
|
||||
)
|
||||
|
||||
func TestFilterIn(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("single-row", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -79,6 +79,8 @@ func TestFilterIn(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("const-column", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -130,6 +132,8 @@ func TestFilterIn(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("dict", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -185,6 +189,8 @@ func TestFilterIn(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("strings", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -237,6 +243,8 @@ func TestFilterIn(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("uint8", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -302,6 +310,8 @@ func TestFilterIn(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("uint16", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -367,6 +377,8 @@ func TestFilterIn(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("uint32", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -432,6 +444,8 @@ func TestFilterIn(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("uint64", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -491,6 +505,8 @@ func TestFilterIn(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("int64", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -550,6 +566,8 @@ func TestFilterIn(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("float64", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -631,6 +649,8 @@ func TestFilterIn(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("ipv4", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -697,6 +717,8 @@ func TestFilterIn(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("timestamp-iso8601", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "_msg",
|
||||
@@ -752,9 +774,6 @@ func TestFilterIn(t *testing.T) {
|
||||
}
|
||||
testFilterMatchForColumns(t, columns, fi, "_msg", nil)
|
||||
})
|
||||
|
||||
// Remove the remaining data files for the test
|
||||
fs.MustRemoveAll(t.Name())
|
||||
}
|
||||
|
||||
func TestGetCommonTokensAndTokenSets(t *testing.T) {
|
||||
|
||||
@@ -2,8 +2,6 @@ package logstorage
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fs"
|
||||
)
|
||||
|
||||
func TestMatchIPv4Range(t *testing.T) {
|
||||
@@ -35,6 +33,8 @@ func TestFilterIPv4Range(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("const-column", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -85,6 +85,8 @@ func TestFilterIPv4Range(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("dict", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -140,6 +142,8 @@ func TestFilterIPv4Range(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("strings", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -183,6 +187,8 @@ func TestFilterIPv4Range(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("uint8", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -212,6 +218,8 @@ func TestFilterIPv4Range(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("uint16", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -241,6 +249,8 @@ func TestFilterIPv4Range(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("uint32", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -270,6 +280,8 @@ func TestFilterIPv4Range(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("uint64", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -299,6 +311,8 @@ func TestFilterIPv4Range(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("int64", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -328,6 +342,8 @@ func TestFilterIPv4Range(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("float64", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -357,6 +373,8 @@ func TestFilterIPv4Range(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("ipv4", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -409,6 +427,8 @@ func TestFilterIPv4Range(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("timestamp-iso8601", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "_msg",
|
||||
@@ -434,7 +454,4 @@ func TestFilterIPv4Range(t *testing.T) {
|
||||
}
|
||||
testFilterMatchForColumns(t, columns, fr, "_msg", nil)
|
||||
})
|
||||
|
||||
// Remove the remaining data files for the test
|
||||
fs.MustRemoveAll(t.Name())
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@ package logstorage
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fs"
|
||||
)
|
||||
|
||||
func TestMatchLenRange(t *testing.T) {
|
||||
@@ -38,6 +36,8 @@ func TestFilterLenRange(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("const-column", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -81,6 +81,8 @@ func TestFilterLenRange(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("dict", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -121,6 +123,8 @@ func TestFilterLenRange(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("strings", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -157,6 +161,8 @@ func TestFilterLenRange(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("uint8", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -201,6 +207,8 @@ func TestFilterLenRange(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("uint16", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -245,6 +253,8 @@ func TestFilterLenRange(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("uint32", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -289,6 +299,8 @@ func TestFilterLenRange(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("uint64", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -333,6 +345,8 @@ func TestFilterLenRange(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("int64", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -377,6 +391,8 @@ func TestFilterLenRange(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("float64", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -414,6 +430,8 @@ func TestFilterLenRange(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("ipv4", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -452,6 +470,8 @@ func TestFilterLenRange(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("timestamp-iso8601", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "_msg",
|
||||
@@ -485,7 +505,4 @@ func TestFilterLenRange(t *testing.T) {
|
||||
}
|
||||
testFilterMatchForColumns(t, columns, fr, "_msg", nil)
|
||||
})
|
||||
|
||||
// Remove the remaining data files for the test
|
||||
fs.MustRemoveAll(t.Name())
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@ package logstorage
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fs"
|
||||
)
|
||||
|
||||
func TestMatchPhrase(t *testing.T) {
|
||||
@@ -51,6 +49,8 @@ func TestFilterPhrase(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("single-row", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -124,6 +124,8 @@ func TestFilterPhrase(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("const-column", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "other-column",
|
||||
@@ -233,6 +235,8 @@ func TestFilterPhrase(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("dict", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -288,6 +292,8 @@ func TestFilterPhrase(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("strings", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -358,6 +364,8 @@ func TestFilterPhrase(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("uint8", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -423,6 +431,8 @@ func TestFilterPhrase(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("uint16", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -487,6 +497,8 @@ func TestFilterPhrase(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("uint32", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -551,6 +563,8 @@ func TestFilterPhrase(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("uint64", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -614,6 +628,8 @@ func TestFilterPhrase(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("int64", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -677,6 +693,8 @@ func TestFilterPhrase(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("float64", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -782,6 +800,8 @@ func TestFilterPhrase(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("ipv4", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -878,6 +898,8 @@ func TestFilterPhrase(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("timestamp-iso8601", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "_msg",
|
||||
@@ -959,7 +981,4 @@ func TestFilterPhrase(t *testing.T) {
|
||||
}
|
||||
testFilterMatchForColumns(t, columns, pf, "_msg", nil)
|
||||
})
|
||||
|
||||
// Remove the remaining data files for the test
|
||||
fs.MustRemoveAll(t.Name())
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@ package logstorage
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fs"
|
||||
)
|
||||
|
||||
func TestMatchPrefix(t *testing.T) {
|
||||
@@ -51,6 +49,8 @@ func TestFilterPrefix(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("single-row", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -136,6 +136,8 @@ func TestFilterPrefix(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("const-column", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "other-column",
|
||||
@@ -251,6 +253,8 @@ func TestFilterPrefix(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("dict", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -306,6 +310,8 @@ func TestFilterPrefix(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("strings", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -388,6 +394,8 @@ func TestFilterPrefix(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("uint8", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -453,6 +461,8 @@ func TestFilterPrefix(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("uint16", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -517,6 +527,8 @@ func TestFilterPrefix(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("uint32", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -581,6 +593,8 @@ func TestFilterPrefix(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("uint64", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -644,6 +658,8 @@ func TestFilterPrefix(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("float64", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -755,6 +771,8 @@ func TestFilterPrefix(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("ipv4", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -851,6 +869,8 @@ func TestFilterPrefix(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("timestamp-iso8601", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "_msg",
|
||||
@@ -932,7 +952,4 @@ func TestFilterPrefix(t *testing.T) {
|
||||
}
|
||||
testFilterMatchForColumns(t, columns, fp, "_msg", nil)
|
||||
})
|
||||
|
||||
// Remove the remaining data files for the test
|
||||
fs.MustRemoveAll(t.Name())
|
||||
}
|
||||
|
||||
@@ -2,14 +2,14 @@ package logstorage
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fs"
|
||||
)
|
||||
|
||||
func TestFilterRange(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("const-column", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -81,6 +81,8 @@ func TestFilterRange(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("dict", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -150,6 +152,8 @@ func TestFilterRange(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("strings", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -214,6 +218,8 @@ func TestFilterRange(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("uint8", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -280,6 +286,8 @@ func TestFilterRange(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("uint16", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -345,6 +353,8 @@ func TestFilterRange(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("uint32", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -410,6 +420,8 @@ func TestFilterRange(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("uint64", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -482,6 +494,8 @@ func TestFilterRange(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("int64", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -554,6 +568,8 @@ func TestFilterRange(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("float64", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -626,6 +642,8 @@ func TestFilterRange(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("ipv4", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -655,6 +673,8 @@ func TestFilterRange(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("timestamp-iso8601", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "_msg",
|
||||
@@ -680,7 +700,4 @@ func TestFilterRange(t *testing.T) {
|
||||
}
|
||||
testFilterMatchForColumns(t, columns, fr, "_msg", nil)
|
||||
})
|
||||
|
||||
// Remove the remaining data files for the test
|
||||
fs.MustRemoveAll(t.Name())
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fs"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/regexutil"
|
||||
)
|
||||
|
||||
@@ -12,6 +11,8 @@ func TestFilterRegexp(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("const-column", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -57,6 +58,8 @@ func TestFilterRegexp(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("dict", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -95,6 +98,8 @@ func TestFilterRegexp(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("strings", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -129,6 +134,8 @@ func TestFilterRegexp(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("uint8", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -164,6 +171,8 @@ func TestFilterRegexp(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("uint16", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -199,6 +208,8 @@ func TestFilterRegexp(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("uint32", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -234,6 +245,8 @@ func TestFilterRegexp(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("uint64", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -269,6 +282,8 @@ func TestFilterRegexp(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("float64", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -304,6 +319,8 @@ func TestFilterRegexp(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("ipv4", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "foo",
|
||||
@@ -340,6 +357,8 @@ func TestFilterRegexp(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("timestamp-iso8601", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
columns := []column{
|
||||
{
|
||||
name: "_msg",
|
||||
@@ -371,9 +390,6 @@ func TestFilterRegexp(t *testing.T) {
|
||||
}
|
||||
testFilterMatchForColumns(t, columns, fr, "_msg", nil)
|
||||
})
|
||||
|
||||
// Remove the remaining data files for the test
|
||||
fs.MustRemoveAll(t.Name())
|
||||
}
|
||||
|
||||
func TestSkipFirstLastToken(t *testing.T) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user