mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2025-01-20 07:19:17 +01:00
added reusable templates support (#2532)
Signed-off-by: Andrii Chubatiuk <andrew.chubatiuk@gmail.com>
This commit is contained in:
parent
9d7da130b5
commit
a531a96193
@ -62,6 +62,7 @@ publish-vmalert:
|
|||||||
|
|
||||||
test-vmalert:
|
test-vmalert:
|
||||||
go test -v -race -cover ./app/vmalert -loggerLevel=ERROR
|
go test -v -race -cover ./app/vmalert -loggerLevel=ERROR
|
||||||
|
go test -v -race -cover ./app/vmalert/templates
|
||||||
go test -v -race -cover ./app/vmalert/datasource
|
go test -v -race -cover ./app/vmalert/datasource
|
||||||
go test -v -race -cover ./app/vmalert/notifier
|
go test -v -race -cover ./app/vmalert/notifier
|
||||||
go test -v -race -cover ./app/vmalert/config
|
go test -v -race -cover ./app/vmalert/config
|
||||||
|
@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/config"
|
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/config"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/datasource"
|
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/datasource"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/notifier"
|
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/notifier"
|
||||||
|
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/templates"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/utils"
|
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/utils"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
|
||||||
@ -152,7 +153,7 @@ type labelSet struct {
|
|||||||
|
|
||||||
// toLabels converts labels from given Metric
|
// toLabels converts labels from given Metric
|
||||||
// to labelSet which contains original and processed labels.
|
// to labelSet which contains original and processed labels.
|
||||||
func (ar *AlertingRule) toLabels(m datasource.Metric, qFn notifier.QueryFn) (*labelSet, error) {
|
func (ar *AlertingRule) toLabels(m datasource.Metric, qFn templates.QueryFn) (*labelSet, error) {
|
||||||
ls := &labelSet{
|
ls := &labelSet{
|
||||||
origin: make(map[string]string, len(m.Labels)),
|
origin: make(map[string]string, len(m.Labels)),
|
||||||
processed: make(map[string]string),
|
processed: make(map[string]string),
|
||||||
@ -382,7 +383,7 @@ func hash(labels map[string]string) uint64 {
|
|||||||
return hash.Sum64()
|
return hash.Sum64()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ar *AlertingRule) newAlert(m datasource.Metric, ls *labelSet, start time.Time, qFn notifier.QueryFn) (*notifier.Alert, error) {
|
func (ar *AlertingRule) newAlert(m datasource.Metric, ls *labelSet, start time.Time, qFn templates.QueryFn) (*notifier.Alert, error) {
|
||||||
var err error
|
var err error
|
||||||
if ls == nil {
|
if ls == nil {
|
||||||
ls, err = ar.toLabels(m, qFn)
|
ls, err = ar.toLabels(m, qFn)
|
||||||
|
@ -10,18 +10,19 @@ import (
|
|||||||
"gopkg.in/yaml.v2"
|
"gopkg.in/yaml.v2"
|
||||||
|
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/datasource"
|
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/datasource"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/notifier"
|
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/templates"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promutils"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
func TestMain(m *testing.M) {
|
||||||
u, _ := url.Parse("https://victoriametrics.com/path")
|
if err := templates.Load([]string{"testdata/templates/*good.tmpl"}, true); err != nil {
|
||||||
notifier.InitTemplateFunc(u)
|
os.Exit(1)
|
||||||
|
}
|
||||||
os.Exit(m.Run())
|
os.Exit(m.Run())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseGood(t *testing.T) {
|
func TestParseGood(t *testing.T) {
|
||||||
if _, err := Parse([]string{"testdata/*good.rules", "testdata/dir/*good.*"}, true, true); err != nil {
|
if _, err := Parse([]string{"testdata/rules/*good.rules", "testdata/dir/*good.*"}, true, true); err != nil {
|
||||||
t.Errorf("error parsing files %s", err)
|
t.Errorf("error parsing files %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -32,7 +33,7 @@ func TestParseBad(t *testing.T) {
|
|||||||
expErr string
|
expErr string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
[]string{"testdata/rules0-bad.rules"},
|
[]string{"testdata/rules/rules0-bad.rules"},
|
||||||
"unexpected token",
|
"unexpected token",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -56,7 +57,7 @@ func TestParseBad(t *testing.T) {
|
|||||||
"either `record` or `alert` must be set",
|
"either `record` or `alert` must be set",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
[]string{"testdata/rules1-bad.rules"},
|
[]string{"testdata/rules/rules1-bad.rules"},
|
||||||
"bad graphite expr",
|
"bad graphite expr",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
3
app/vmalert/config/testdata/templates/templates0-good.tmpl
vendored
Normal file
3
app/vmalert/config/testdata/templates/templates0-good.tmpl
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{{ define "template0" }}
|
||||||
|
Visit {{ externalURL }}
|
||||||
|
{{ end }}
|
3
app/vmalert/config/testdata/templates/templates1-good.tmpl
vendored
Normal file
3
app/vmalert/config/testdata/templates/templates1-good.tmpl
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{{ define "template1" }}
|
||||||
|
{{ 1048576 | humanize1024 }}
|
||||||
|
{{ end }}
|
3
app/vmalert/config/testdata/templates/templates2-good.tmpl
vendored
Normal file
3
app/vmalert/config/testdata/templates/templates2-good.tmpl
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{{ define "template2" }}
|
||||||
|
{{ 1048576 | humanize1024 }}
|
||||||
|
{{ end }}
|
3
app/vmalert/config/testdata/templates/templates3-good.tmpl
vendored
Normal file
3
app/vmalert/config/testdata/templates/templates3-good.tmpl
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{{ define "template3" }}
|
||||||
|
{{ printf "%s to %s!" "welcome" "hell" | toUpper }}
|
||||||
|
{{ end }}
|
3
app/vmalert/config/testdata/templates/templates4-good-tmpl
vendored
Normal file
3
app/vmalert/config/testdata/templates/templates4-good-tmpl
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{{ define "template3" }}
|
||||||
|
{{ 1230912039102391023.0 | humanizeDuration }}
|
||||||
|
{{ end }}
|
@ -157,7 +157,7 @@ func TestUpdateWith(t *testing.T) {
|
|||||||
|
|
||||||
func TestGroupStart(t *testing.T) {
|
func TestGroupStart(t *testing.T) {
|
||||||
// TODO: make parsing from string instead of file
|
// TODO: make parsing from string instead of file
|
||||||
groups, err := config.Parse([]string{"config/testdata/rules1-good.rules"}, true, true)
|
groups, err := config.Parse([]string{"config/testdata/rules/rules1-good.rules"}, true, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to parse rules: %s", err)
|
t.Fatalf("failed to parse rules: %s", err)
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,7 @@ import (
|
|||||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/notifier"
|
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/notifier"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/remoteread"
|
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/remoteread"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/remotewrite"
|
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/remotewrite"
|
||||||
|
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/templates"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/buildinfo"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/buildinfo"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/envflag"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/envflag"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fasttime"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fasttime"
|
||||||
@ -34,6 +35,13 @@ Examples:
|
|||||||
absolute path to all .yaml files in root.
|
absolute path to all .yaml files in root.
|
||||||
Rule files may contain %{ENV_VAR} placeholders, which are substituted by the corresponding env vars.`)
|
Rule files may contain %{ENV_VAR} placeholders, which are substituted by the corresponding env vars.`)
|
||||||
|
|
||||||
|
ruleTemplatesPath = flagutil.NewArray("rule.templates", `Path or glob pattern to location with go template definitions
|
||||||
|
for rules annotations templating. Flag can be specified multiple times.
|
||||||
|
Examples:
|
||||||
|
-rule.templates="/path/to/file". Path to a single file with go templates
|
||||||
|
-rule.templates="dir/*.tpl" -rule.templates="/*.tpl". Relative path to all .tpl files in "dir" folder,
|
||||||
|
absolute path to all .tpl files in root.`)
|
||||||
|
|
||||||
rulesCheckInterval = flag.Duration("rule.configCheckInterval", 0, "Interval for checking for changes in '-rule' files. "+
|
rulesCheckInterval = flag.Duration("rule.configCheckInterval", 0, "Interval for checking for changes in '-rule' files. "+
|
||||||
"By default the checking is disabled. Send SIGHUP signal in order to force config check for changes. DEPRECATED - see '-configCheckInterval' instead")
|
"By default the checking is disabled. Send SIGHUP signal in order to force config check for changes. DEPRECATED - see '-configCheckInterval' instead")
|
||||||
|
|
||||||
@ -73,10 +81,12 @@ func main() {
|
|||||||
envflag.Parse()
|
envflag.Parse()
|
||||||
buildinfo.Init()
|
buildinfo.Init()
|
||||||
logger.Init()
|
logger.Init()
|
||||||
|
err := templates.Load(*ruleTemplatesPath, true)
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatalf("failed to parse %q: %s", *ruleTemplatesPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
if *dryRun {
|
if *dryRun {
|
||||||
u, _ := url.Parse("https://victoriametrics.com/")
|
|
||||||
notifier.InitTemplateFunc(u)
|
|
||||||
groups, err := config.Parse(*rulePath, true, true)
|
groups, err := config.Parse(*rulePath, true, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatalf("failed to parse %q: %s", *rulePath, err)
|
logger.Fatalf("failed to parse %q: %s", *rulePath, err)
|
||||||
@ -91,7 +101,7 @@ func main() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatalf("failed to init `external.url`: %s", err)
|
logger.Fatalf("failed to init `external.url`: %s", err)
|
||||||
}
|
}
|
||||||
notifier.InitTemplateFunc(eu)
|
|
||||||
alertURLGeneratorFn, err = getAlertURLGenerator(eu, *externalAlertSource, *validateTemplates)
|
alertURLGeneratorFn, err = getAlertURLGenerator(eu, *externalAlertSource, *validateTemplates)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatalf("failed to init `external.alert.source`: %s", err)
|
logger.Fatalf("failed to init `external.alert.source`: %s", err)
|
||||||
@ -105,7 +115,6 @@ func main() {
|
|||||||
if rw == nil {
|
if rw == nil {
|
||||||
logger.Fatalf("remoteWrite.url can't be empty in replay mode")
|
logger.Fatalf("remoteWrite.url can't be empty in replay mode")
|
||||||
}
|
}
|
||||||
notifier.InitTemplateFunc(eu)
|
|
||||||
groupsCfg, err := config.Parse(*rulePath, *validateTemplates, *validateExpressions)
|
groupsCfg, err := config.Parse(*rulePath, *validateTemplates, *validateExpressions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatalf("cannot parse configuration file: %s", err)
|
logger.Fatalf("cannot parse configuration file: %s", err)
|
||||||
@ -127,7 +136,6 @@ func main() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatalf("failed to init: %s", err)
|
logger.Fatalf("failed to init: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Infof("reading rules configuration file from %q", strings.Join(*rulePath, ";"))
|
logger.Infof("reading rules configuration file from %q", strings.Join(*rulePath, ";"))
|
||||||
groupsCfg, err := config.Parse(*rulePath, *validateTemplates, *validateExpressions)
|
groupsCfg, err := config.Parse(*rulePath, *validateTemplates, *validateExpressions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -281,7 +289,11 @@ func configReload(ctx context.Context, m *manager, groupsCfg []config.Group, sig
|
|||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
case <-sighupCh:
|
case <-sighupCh:
|
||||||
logger.Infof("SIGHUP received. Going to reload rules %q ...", *rulePath)
|
tmplMsg := ""
|
||||||
|
if len(*ruleTemplatesPath) > 0 {
|
||||||
|
tmplMsg = fmt.Sprintf("and templates %q ", *ruleTemplatesPath)
|
||||||
|
}
|
||||||
|
logger.Infof("SIGHUP received. Going to reload rules %q %s...", *rulePath, tmplMsg)
|
||||||
configReloads.Inc()
|
configReloads.Inc()
|
||||||
case <-configCheckCh:
|
case <-configCheckCh:
|
||||||
}
|
}
|
||||||
@ -291,6 +303,13 @@ func configReload(ctx context.Context, m *manager, groupsCfg []config.Group, sig
|
|||||||
logger.Errorf("failed to reload notifier config: %s", err)
|
logger.Errorf("failed to reload notifier config: %s", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
err := templates.Load(*ruleTemplatesPath, false)
|
||||||
|
if err != nil {
|
||||||
|
configReloadErrors.Inc()
|
||||||
|
configSuccess.Set(0)
|
||||||
|
logger.Errorf("failed to load new templates: %s", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
newGroupsCfg, err := config.Parse(*rulePath, *validateTemplates, *validateExpressions)
|
newGroupsCfg, err := config.Parse(*rulePath, *validateTemplates, *validateExpressions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
configReloadErrors.Inc()
|
configReloadErrors.Inc()
|
||||||
@ -299,6 +318,7 @@ func configReload(ctx context.Context, m *manager, groupsCfg []config.Group, sig
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if configsEqual(newGroupsCfg, groupsCfg) {
|
if configsEqual(newGroupsCfg, groupsCfg) {
|
||||||
|
templates.Reload()
|
||||||
// set success to 1 since previous reload
|
// set success to 1 since previous reload
|
||||||
// could have been unsuccessful
|
// could have been unsuccessful
|
||||||
configSuccess.Set(1)
|
configSuccess.Set(1)
|
||||||
@ -311,6 +331,7 @@ func configReload(ctx context.Context, m *manager, groupsCfg []config.Group, sig
|
|||||||
logger.Errorf("error while reloading rules: %s", err)
|
logger.Errorf("error while reloading rules: %s", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
templates.Reload()
|
||||||
groupsCfg = newGroupsCfg
|
groupsCfg = newGroupsCfg
|
||||||
configSuccess.Set(1)
|
configSuccess.Set(1)
|
||||||
configTimestamp.Set(fasttime.UnixTimestamp())
|
configTimestamp.Set(fasttime.UnixTimestamp())
|
||||||
|
@ -3,7 +3,6 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net/url"
|
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@ -14,11 +13,13 @@ import (
|
|||||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/datasource"
|
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/datasource"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/notifier"
|
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/notifier"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/remotewrite"
|
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/remotewrite"
|
||||||
|
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/templates"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
func TestMain(m *testing.M) {
|
||||||
u, _ := url.Parse("https://victoriametrics.com/path")
|
if err := templates.Load([]string{"testdata/templates/*good.tmpl"}, true); err != nil {
|
||||||
notifier.InitTemplateFunc(u)
|
os.Exit(1)
|
||||||
|
}
|
||||||
os.Exit(m.Run())
|
os.Exit(m.Run())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,9 +48,9 @@ func TestManagerUpdateConcurrent(t *testing.T) {
|
|||||||
"config/testdata/dir/rules0-bad.rules",
|
"config/testdata/dir/rules0-bad.rules",
|
||||||
"config/testdata/dir/rules1-good.rules",
|
"config/testdata/dir/rules1-good.rules",
|
||||||
"config/testdata/dir/rules1-bad.rules",
|
"config/testdata/dir/rules1-bad.rules",
|
||||||
"config/testdata/rules0-good.rules",
|
"config/testdata/rules/rules0-good.rules",
|
||||||
"config/testdata/rules1-good.rules",
|
"config/testdata/rules/rules1-good.rules",
|
||||||
"config/testdata/rules2-good.rules",
|
"config/testdata/rules/rules2-good.rules",
|
||||||
}
|
}
|
||||||
evalInterval := *evaluationInterval
|
evalInterval := *evaluationInterval
|
||||||
defer func() { *evaluationInterval = evalInterval }()
|
defer func() { *evaluationInterval = evalInterval }()
|
||||||
@ -125,7 +126,7 @@ func TestManagerUpdate(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "update good rules",
|
name: "update good rules",
|
||||||
initPath: "config/testdata/rules0-good.rules",
|
initPath: "config/testdata/rules/rules0-good.rules",
|
||||||
updatePath: "config/testdata/dir/rules1-good.rules",
|
updatePath: "config/testdata/dir/rules1-good.rules",
|
||||||
want: []*Group{
|
want: []*Group{
|
||||||
{
|
{
|
||||||
@ -150,18 +151,18 @@ func TestManagerUpdate(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "update good rules from 1 to 2 groups",
|
name: "update good rules from 1 to 2 groups",
|
||||||
initPath: "config/testdata/dir/rules1-good.rules",
|
initPath: "config/testdata/dir/rules/rules1-good.rules",
|
||||||
updatePath: "config/testdata/rules0-good.rules",
|
updatePath: "config/testdata/rules/rules0-good.rules",
|
||||||
want: []*Group{
|
want: []*Group{
|
||||||
{
|
{
|
||||||
File: "config/testdata/rules0-good.rules",
|
File: "config/testdata/rules/rules0-good.rules",
|
||||||
Name: "groupGorSingleAlert",
|
Name: "groupGorSingleAlert",
|
||||||
Type: datasource.NewPrometheusType(),
|
Type: datasource.NewPrometheusType(),
|
||||||
Rules: []Rule{VMRows},
|
Rules: []Rule{VMRows},
|
||||||
Interval: defaultEvalInterval,
|
Interval: defaultEvalInterval,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
File: "config/testdata/rules0-good.rules",
|
File: "config/testdata/rules/rules0-good.rules",
|
||||||
Interval: defaultEvalInterval,
|
Interval: defaultEvalInterval,
|
||||||
Type: datasource.NewPrometheusType(),
|
Type: datasource.NewPrometheusType(),
|
||||||
Name: "TestGroup", Rules: []Rule{
|
Name: "TestGroup", Rules: []Rule{
|
||||||
@ -172,18 +173,18 @@ func TestManagerUpdate(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "update with one bad rule file",
|
name: "update with one bad rule file",
|
||||||
initPath: "config/testdata/rules0-good.rules",
|
initPath: "config/testdata/rules/rules0-good.rules",
|
||||||
updatePath: "config/testdata/dir/rules2-bad.rules",
|
updatePath: "config/testdata/dir/rules2-bad.rules",
|
||||||
want: []*Group{
|
want: []*Group{
|
||||||
{
|
{
|
||||||
File: "config/testdata/rules0-good.rules",
|
File: "config/testdata/rules/rules0-good.rules",
|
||||||
Name: "groupGorSingleAlert",
|
Name: "groupGorSingleAlert",
|
||||||
Type: datasource.NewPrometheusType(),
|
Type: datasource.NewPrometheusType(),
|
||||||
Interval: defaultEvalInterval,
|
Interval: defaultEvalInterval,
|
||||||
Rules: []Rule{VMRows},
|
Rules: []Rule{VMRows},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
File: "config/testdata/rules0-good.rules",
|
File: "config/testdata/rules/rules0-good.rules",
|
||||||
Interval: defaultEvalInterval,
|
Interval: defaultEvalInterval,
|
||||||
Name: "TestGroup",
|
Name: "TestGroup",
|
||||||
Type: datasource.NewPrometheusType(),
|
Type: datasource.NewPrometheusType(),
|
||||||
@ -196,17 +197,17 @@ func TestManagerUpdate(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "update empty dir rules from 0 to 2 groups",
|
name: "update empty dir rules from 0 to 2 groups",
|
||||||
initPath: "config/testdata/empty/*",
|
initPath: "config/testdata/empty/*",
|
||||||
updatePath: "config/testdata/rules0-good.rules",
|
updatePath: "config/testdata/rules/rules0-good.rules",
|
||||||
want: []*Group{
|
want: []*Group{
|
||||||
{
|
{
|
||||||
File: "config/testdata/rules0-good.rules",
|
File: "config/testdata/rules/rules0-good.rules",
|
||||||
Name: "groupGorSingleAlert",
|
Name: "groupGorSingleAlert",
|
||||||
Type: datasource.NewPrometheusType(),
|
Type: datasource.NewPrometheusType(),
|
||||||
Interval: defaultEvalInterval,
|
Interval: defaultEvalInterval,
|
||||||
Rules: []Rule{VMRows},
|
Rules: []Rule{VMRows},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
File: "config/testdata/rules0-good.rules",
|
File: "config/testdata/rules/rules0-good.rules",
|
||||||
Interval: defaultEvalInterval,
|
Interval: defaultEvalInterval,
|
||||||
Type: datasource.NewPrometheusType(),
|
Type: datasource.NewPrometheusType(),
|
||||||
Name: "TestGroup", Rules: []Rule{
|
Name: "TestGroup", Rules: []Rule{
|
||||||
|
@ -5,9 +5,10 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
"text/template"
|
textTpl "text/template"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/templates"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/utils"
|
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/utils"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promrelabel"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promrelabel"
|
||||||
@ -90,26 +91,38 @@ var tplHeaders = []string{
|
|||||||
// map of annotations.
|
// map of annotations.
|
||||||
// Every alert could have a different datasource, so function
|
// Every alert could have a different datasource, so function
|
||||||
// requires a queryFunction as an argument.
|
// requires a queryFunction as an argument.
|
||||||
func (a *Alert) ExecTemplate(q QueryFn, labels, annotations map[string]string) (map[string]string, error) {
|
func (a *Alert) ExecTemplate(q templates.QueryFn, labels, annotations map[string]string) (map[string]string, error) {
|
||||||
tplData := AlertTplData{Value: a.Value, Labels: labels, Expr: a.Expr}
|
tplData := AlertTplData{Value: a.Value, Labels: labels, Expr: a.Expr}
|
||||||
return templateAnnotations(annotations, tplData, funcsWithQuery(q), true)
|
tmpl, err := templates.GetWithFuncs(templates.FuncsWithQuery(q))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error getting a template: %w", err)
|
||||||
|
}
|
||||||
|
return templateAnnotations(annotations, tplData, tmpl, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExecTemplate executes the given template for given annotations map.
|
// ExecTemplate executes the given template for given annotations map.
|
||||||
func ExecTemplate(q QueryFn, annotations map[string]string, tpl AlertTplData) (map[string]string, error) {
|
func ExecTemplate(q templates.QueryFn, annotations map[string]string, tplData AlertTplData) (map[string]string, error) {
|
||||||
return templateAnnotations(annotations, tpl, funcsWithQuery(q), true)
|
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
|
// ValidateTemplates validate annotations for possible template error, uses empty data for template population
|
||||||
func ValidateTemplates(annotations map[string]string) error {
|
func ValidateTemplates(annotations map[string]string) error {
|
||||||
_, err := templateAnnotations(annotations, AlertTplData{
|
tmpl, err := templates.Get()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = templateAnnotations(annotations, AlertTplData{
|
||||||
Labels: map[string]string{},
|
Labels: map[string]string{},
|
||||||
Value: 0,
|
Value: 0,
|
||||||
}, tmplFunc, false)
|
}, tmpl, false)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func templateAnnotations(annotations map[string]string, data AlertTplData, funcs template.FuncMap, execute bool) (map[string]string, error) {
|
func templateAnnotations(annotations map[string]string, data AlertTplData, tmpl *textTpl.Template, execute bool) (map[string]string, error) {
|
||||||
var builder strings.Builder
|
var builder strings.Builder
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
eg := new(utils.ErrGroup)
|
eg := new(utils.ErrGroup)
|
||||||
@ -122,7 +135,7 @@ func templateAnnotations(annotations map[string]string, data AlertTplData, funcs
|
|||||||
builder.Grow(len(header) + len(text))
|
builder.Grow(len(header) + len(text))
|
||||||
builder.WriteString(header)
|
builder.WriteString(header)
|
||||||
builder.WriteString(text)
|
builder.WriteString(text)
|
||||||
if err := templateAnnotation(&buf, builder.String(), tData, funcs, execute); err != nil {
|
if err := templateAnnotation(&buf, builder.String(), tData, tmpl, execute); err != nil {
|
||||||
r[key] = text
|
r[key] = text
|
||||||
eg.Add(fmt.Errorf("key %q, template %q: %w", key, text, err))
|
eg.Add(fmt.Errorf("key %q, template %q: %w", key, text, err))
|
||||||
continue
|
continue
|
||||||
@ -138,11 +151,17 @@ type tplData struct {
|
|||||||
ExternalURL string
|
ExternalURL string
|
||||||
}
|
}
|
||||||
|
|
||||||
func templateAnnotation(dst io.Writer, text string, data tplData, funcs template.FuncMap, execute bool) error {
|
func templateAnnotation(dst io.Writer, text string, data tplData, tmpl *textTpl.Template, execute bool) error {
|
||||||
t := template.New("").Funcs(funcs).Option("missingkey=zero")
|
tpl, err := tmpl.Clone()
|
||||||
tpl, err := t.Parse(text)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error parsing annotation: %w", err)
|
return fmt.Errorf("error cloning template before parse annotation: %w", err)
|
||||||
|
}
|
||||||
|
tpl, err = tpl.Parse(text)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error parsing annotation template: %w", err)
|
||||||
|
}
|
||||||
|
if !execute {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
if !execute {
|
if !execute {
|
||||||
return nil
|
return nil
|
||||||
|
@ -3,9 +3,11 @@ package notifier
|
|||||||
import (
|
import (
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/templates"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promauth"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promauth"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
|
||||||
@ -83,6 +85,12 @@ func Init(gen AlertURLGenerator, extLabels map[string]string, extURL string) (fu
|
|||||||
|
|
||||||
externalURL = extURL
|
externalURL = extURL
|
||||||
externalLabels = extLabels
|
externalLabels = extLabels
|
||||||
|
eu, err := url.Parse(externalURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse external URL: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
templates.UpdateWithFuncs(templates.FuncsWithExternalURL(eu))
|
||||||
|
|
||||||
if *configPath == "" && len(*addrs) == 0 {
|
if *configPath == "" && len(*addrs) == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@ -102,7 +110,6 @@ func Init(gen AlertURLGenerator, extLabels map[string]string, extURL string) (fu
|
|||||||
return staticNotifiersFn, nil
|
return staticNotifiersFn, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
|
||||||
cw, err = newWatcher(*configPath, gen)
|
cw, err = newWatcher(*configPath, gen)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to init config watcher: %s", err)
|
return nil, fmt.Errorf("failed to init config watcher: %s", err)
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
package notifier
|
package notifier
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/url"
|
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/templates"
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
func TestMain(m *testing.M) {
|
||||||
u, _ := url.Parse("https://victoriametrics.com/path")
|
if err := templates.Load([]string{"testdata/templates/*good.tmpl"}, true); err != nil {
|
||||||
InitTemplateFunc(u)
|
os.Exit(1)
|
||||||
|
}
|
||||||
os.Exit(m.Run())
|
os.Exit(m.Run())
|
||||||
}
|
}
|
||||||
|
@ -11,26 +11,117 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
package notifier
|
package templates
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
htmlTpl "html/template"
|
||||||
|
"io/ioutil"
|
||||||
"math"
|
"math"
|
||||||
"net"
|
"net"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
htmlTpl "html/template"
|
|
||||||
textTpl "text/template"
|
textTpl "text/template"
|
||||||
|
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/datasource"
|
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/datasource"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promutils"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// go template execution fails when it's tree is empty
|
||||||
|
const defaultTemplate = `{{- define "default.template" -}}{{- end -}}`
|
||||||
|
|
||||||
|
var tplMu sync.RWMutex
|
||||||
|
|
||||||
|
type textTemplate struct {
|
||||||
|
current *textTpl.Template
|
||||||
|
replacement *textTpl.Template
|
||||||
|
}
|
||||||
|
|
||||||
|
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 undefined or with overwrite=true
|
||||||
|
// or sets replacement templates and adds templates with new names to a current
|
||||||
|
func Load(pathPatterns []string, overwrite bool) error {
|
||||||
|
var err error
|
||||||
|
tmpl := newTemplate()
|
||||||
|
for _, tp := range pathPatterns {
|
||||||
|
p, err := filepath.Glob(tp)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to retrieve a template glob %q: %w", tp, err)
|
||||||
|
}
|
||||||
|
if len(p) > 0 {
|
||||||
|
tmpl, err = tmpl.ParseGlob(tp)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to parse template glob %q: %w", tp, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(tmpl.Templates()) > 0 {
|
||||||
|
err := tmpl.Execute(ioutil.Discard, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to execute template: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tplMu.Lock()
|
||||||
|
defer tplMu.Unlock()
|
||||||
|
if masterTmpl.current == nil || overwrite {
|
||||||
|
masterTmpl.replacement = nil
|
||||||
|
masterTmpl.current = newTemplate()
|
||||||
|
} else {
|
||||||
|
masterTmpl.replacement = newTemplate()
|
||||||
|
if err = copyTemplates(tmpl, masterTmpl.replacement, overwrite); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return copyTemplates(tmpl, masterTmpl.current, overwrite)
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyTemplates(from *textTpl.Template, to *textTpl.Template, overwrite bool) error {
|
||||||
|
if from == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if to == nil {
|
||||||
|
to = newTemplate()
|
||||||
|
}
|
||||||
|
tmpl, err := from.Clone()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, t := range tmpl.Templates() {
|
||||||
|
if to.Lookup(t.Name()) == nil || overwrite {
|
||||||
|
to, err = to.AddParseTree(t.Name(), t.Tree)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to add template %q: %w", t.Name(), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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,
|
// metric is private copy of datasource.Metric,
|
||||||
// it is used for templating annotations,
|
// it is used for templating annotations,
|
||||||
// Labels as map simplifies templates evaluation.
|
// Labels as map simplifies templates evaluation.
|
||||||
@ -60,12 +151,62 @@ func datasourceMetricsToTemplateMetrics(ms []datasource.Metric) []metric {
|
|||||||
// for templating functions.
|
// for templating functions.
|
||||||
type QueryFn func(query string) ([]datasource.Metric, error)
|
type QueryFn func(query string) ([]datasource.Metric, error)
|
||||||
|
|
||||||
var tmplFunc textTpl.FuncMap
|
// UpdateWithFuncs updates existing or sets a new function map for a template
|
||||||
|
func UpdateWithFuncs(funcs textTpl.FuncMap) {
|
||||||
|
tplMu.Lock()
|
||||||
|
defer tplMu.Unlock()
|
||||||
|
masterTmpl.current = masterTmpl.current.Funcs(funcs)
|
||||||
|
}
|
||||||
|
|
||||||
// InitTemplateFunc initiates template helper functions
|
// GetWithFuncs returns a copy of current template with additional FuncMap
|
||||||
func InitTemplateFunc(externalURL *url.URL) {
|
// provided with funcs argument
|
||||||
|
func GetWithFuncs(funcs textTpl.FuncMap) (*textTpl.Template, error) {
|
||||||
|
tplMu.RLock()
|
||||||
|
defer tplMu.RUnlock()
|
||||||
|
tmpl, err := masterTmpl.current.Clone()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return tmpl.Funcs(funcs), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get returns a copy of a template
|
||||||
|
func Get() (*textTpl.Template, error) {
|
||||||
|
tplMu.RLock()
|
||||||
|
defer tplMu.RUnlock()
|
||||||
|
return masterTmpl.current.Clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
result, err := query(q)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return datasourceMetricsToTemplateMetrics(result), 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()
|
||||||
|
},
|
||||||
|
|
||||||
|
"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/
|
// See https://prometheus.io/docs/prometheus/latest/configuration/template_reference/
|
||||||
tmplFunc = textTpl.FuncMap{
|
return textTpl.FuncMap{
|
||||||
/* Strings */
|
/* Strings */
|
||||||
|
|
||||||
// reReplaceAll ReplaceAllString returns a copy of src, replacing matches of the Regexp with
|
// reReplaceAll ReplaceAllString returns a copy of src, replacing matches of the Regexp with
|
||||||
@ -219,12 +360,22 @@ func InitTemplateFunc(externalURL *url.URL) {
|
|||||||
|
|
||||||
// externalURL returns value of `external.url` flag
|
// externalURL returns value of `external.url` flag
|
||||||
"externalURL": func() string {
|
"externalURL": func() string {
|
||||||
return externalURL.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 returns a Path segment from the URL value in `external.url` flag
|
||||||
"pathPrefix": func() string {
|
"pathPrefix": func() string {
|
||||||
return externalURL.Path
|
// 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,
|
// pathEscape escapes the string so it can be safely placed inside a URL path segment,
|
||||||
@ -259,7 +410,7 @@ func InitTemplateFunc(externalURL *url.URL) {
|
|||||||
// execute "/api/v1/query?query=foo" request and will return
|
// execute "/api/v1/query?query=foo" request and will return
|
||||||
// the first value in response.
|
// the first value in response.
|
||||||
"query": func(q string) ([]metric, error) {
|
"query": func(q string) ([]metric, error) {
|
||||||
// query function supposed to be substituted at funcsWithQuery().
|
// query function supposed to be substituted at FuncsWithQuery().
|
||||||
// it is present here only for validation purposes, when there is no
|
// it is present here only for validation purposes, when there is no
|
||||||
// provided datasource.
|
// provided datasource.
|
||||||
//
|
//
|
||||||
@ -316,21 +467,6 @@ func InitTemplateFunc(externalURL *url.URL) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func funcsWithQuery(query QueryFn) textTpl.FuncMap {
|
|
||||||
fm := make(textTpl.FuncMap)
|
|
||||||
for k, fn := range tmplFunc {
|
|
||||||
fm[k] = fn
|
|
||||||
}
|
|
||||||
fm["query"] = func(q string) ([]metric, error) {
|
|
||||||
result, err := query(q)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return datasourceMetricsToTemplateMetrics(result), nil
|
|
||||||
}
|
|
||||||
return fm
|
|
||||||
}
|
|
||||||
|
|
||||||
// Time is the number of milliseconds since the epoch
|
// Time is the number of milliseconds since the epoch
|
||||||
// (1970-01-01 00:00 UTC) excluding leap seconds.
|
// (1970-01-01 00:00 UTC) excluding leap seconds.
|
||||||
type Time int64
|
type Time int64
|
275
app/vmalert/templates/template_test.go
Normal file
275
app/vmalert/templates/template_test.go
Normal file
@ -0,0 +1,275 @@
|
|||||||
|
package templates
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
textTpl "text/template"
|
||||||
|
)
|
||||||
|
|
||||||
|
func mkTemplate(current, replacement interface{}) 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(t *testing.T, 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
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTemplates_Load(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
initialTemplate textTemplate
|
||||||
|
pathPatterns []string
|
||||||
|
overwrite bool
|
||||||
|
expectedTemplate textTemplate
|
||||||
|
expErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"non existing path undefined template override",
|
||||||
|
mkTemplate(nil, nil),
|
||||||
|
[]string{
|
||||||
|
"templates/non-existing/good-*.tpl",
|
||||||
|
"templates/absent/good-*.tpl",
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
mkTemplate(``, nil),
|
||||||
|
"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"non existing path defined template override",
|
||||||
|
mkTemplate(`
|
||||||
|
{{- define "test.1" -}}
|
||||||
|
{{- printf "value" -}}
|
||||||
|
{{- end -}}
|
||||||
|
`, nil),
|
||||||
|
[]string{
|
||||||
|
"templates/non-existing/good-*.tpl",
|
||||||
|
"templates/absent/good-*.tpl",
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
mkTemplate(``, nil),
|
||||||
|
"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"existing path undefined template override",
|
||||||
|
mkTemplate(nil, nil),
|
||||||
|
[]string{
|
||||||
|
"templates/other/nested/good0-*.tpl",
|
||||||
|
"templates/test/good0-*.tpl",
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
mkTemplate(`
|
||||||
|
{{- define "good0-test.tpl" -}}{{- end -}}
|
||||||
|
{{- define "test.0" -}}
|
||||||
|
{{ printf "Hello %s!" externalURL }}
|
||||||
|
{{- end -}}
|
||||||
|
{{- define "test.1" -}}
|
||||||
|
{{ printf "Hello %s!" externalURL }}
|
||||||
|
{{- end -}}
|
||||||
|
{{- define "test.2" -}}
|
||||||
|
{{ printf "Hello %s!" externalURL }}
|
||||||
|
{{- end -}}
|
||||||
|
{{- define "test.3" -}}
|
||||||
|
{{ printf "Hello %s!" externalURL }}
|
||||||
|
{{- end -}}
|
||||||
|
`, nil),
|
||||||
|
"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"existing path defined template override",
|
||||||
|
mkTemplate(`
|
||||||
|
{{- define "test.1" -}}
|
||||||
|
{{ printf "Hello %s!" "world" }}
|
||||||
|
{{- end -}}
|
||||||
|
`, nil),
|
||||||
|
[]string{
|
||||||
|
"templates/other/nested/good0-*.tpl",
|
||||||
|
"templates/test/good0-*.tpl",
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
mkTemplate(`
|
||||||
|
{{- define "good0-test.tpl" -}}{{- end -}}
|
||||||
|
{{- define "test.0" -}}
|
||||||
|
{{ printf "Hello %s!" externalURL }}
|
||||||
|
{{- end -}}
|
||||||
|
{{- define "test.1" -}}
|
||||||
|
{{ printf "Hello %s!" "world" }}
|
||||||
|
{{- end -}}
|
||||||
|
{{- define "test.2" -}}
|
||||||
|
{{ printf "Hello %s!" externalURL }}
|
||||||
|
{{- end -}}
|
||||||
|
{{- define "test.3" -}}
|
||||||
|
{{ printf "Hello %s!" externalURL }}
|
||||||
|
{{- end -}}
|
||||||
|
`, `
|
||||||
|
{{- define "good0-test.tpl" -}}{{- end -}}
|
||||||
|
{{- define "test.0" -}}
|
||||||
|
{{ printf "Hello %s!" externalURL }}
|
||||||
|
{{- end -}}
|
||||||
|
{{- define "test.1" -}}
|
||||||
|
{{ printf "Hello %s!" externalURL }}
|
||||||
|
{{- end -}}
|
||||||
|
{{- define "test.2" -}}
|
||||||
|
{{ printf "Hello %s!" externalURL }}
|
||||||
|
{{- end -}}
|
||||||
|
{{- define "test.3" -}}
|
||||||
|
{{ printf "Hello %s!" externalURL }}
|
||||||
|
{{- end -}}
|
||||||
|
`),
|
||||||
|
"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"load template with syntax error",
|
||||||
|
mkTemplate(`
|
||||||
|
{{- define "test.1" -}}
|
||||||
|
{{ printf "Hello %s!" "world" }}
|
||||||
|
{{- end -}}
|
||||||
|
`, nil),
|
||||||
|
[]string{
|
||||||
|
"templates/other/nested/bad0-*.tpl",
|
||||||
|
"templates/test/good0-*.tpl",
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
mkTemplate(`
|
||||||
|
{{- define "test.1" -}}
|
||||||
|
{{ printf "Hello %s!" "world" }}
|
||||||
|
{{- end -}}
|
||||||
|
`, nil),
|
||||||
|
"failed to parse template glob",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
masterTmpl = tc.initialTemplate
|
||||||
|
err := Load(tc.pathPatterns, tc.overwrite)
|
||||||
|
if tc.expErr == "" && err != nil {
|
||||||
|
t.Error("happened error that wasn't expected: %w", err)
|
||||||
|
}
|
||||||
|
if tc.expErr != "" && err == nil {
|
||||||
|
t.Error("%+w", err)
|
||||||
|
t.Error("expected error that didn't happend")
|
||||||
|
}
|
||||||
|
if err != nil && !strings.Contains(err.Error(), tc.expErr) {
|
||||||
|
t.Error("%+w", err)
|
||||||
|
t.Error("expected string doesn't exist in error message")
|
||||||
|
}
|
||||||
|
if !equalTemplates(t, masterTmpl.replacement, tc.expectedTemplate.replacement) {
|
||||||
|
t.Fatalf("replacement template is not as expected")
|
||||||
|
}
|
||||||
|
if !equalTemplates(t, masterTmpl.current, tc.expectedTemplate.current) {
|
||||||
|
t.Fatalf("current template is not as expected")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTemplates_Reload(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
initialTemplate textTemplate
|
||||||
|
expectedTemplate textTemplate
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"empty current and replacement templates",
|
||||||
|
mkTemplate(nil, nil),
|
||||||
|
mkTemplate(nil, nil),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"empty current template only",
|
||||||
|
mkTemplate(`
|
||||||
|
{{- define "test.1" -}}
|
||||||
|
{{- printf "value" -}}
|
||||||
|
{{- end -}}
|
||||||
|
`, nil),
|
||||||
|
mkTemplate(`
|
||||||
|
{{- define "test.1" -}}
|
||||||
|
{{- printf "value" -}}
|
||||||
|
{{- end -}}
|
||||||
|
`, nil),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"empty replacement template only",
|
||||||
|
mkTemplate(nil, `
|
||||||
|
{{- define "test.1" -}}
|
||||||
|
{{- printf "value" -}}
|
||||||
|
{{- end -}}
|
||||||
|
`),
|
||||||
|
mkTemplate(`
|
||||||
|
{{- define "test.1" -}}
|
||||||
|
{{- printf "value" -}}
|
||||||
|
{{- end -}}
|
||||||
|
`, nil),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"defined both templates",
|
||||||
|
mkTemplate(`
|
||||||
|
{{- define "test.0" -}}
|
||||||
|
{{- printf "value" -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- define "test.1" -}}
|
||||||
|
{{- printf "before" -}}
|
||||||
|
{{- end -}}
|
||||||
|
`, `
|
||||||
|
{{- define "test.1" -}}
|
||||||
|
{{- printf "after" -}}
|
||||||
|
{{- end -}}
|
||||||
|
`),
|
||||||
|
mkTemplate(`
|
||||||
|
{{- define "test.1" -}}
|
||||||
|
{{- printf "after" -}}
|
||||||
|
{{- end -}}
|
||||||
|
`, nil),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
masterTmpl = tc.initialTemplate
|
||||||
|
Reload()
|
||||||
|
if !equalTemplates(t, masterTmpl.replacement, tc.expectedTemplate.replacement) {
|
||||||
|
t.Fatalf("replacement template is not as expected")
|
||||||
|
}
|
||||||
|
if !equalTemplates(t, masterTmpl.current, tc.expectedTemplate.current) {
|
||||||
|
t.Fatalf("current template is not as expected")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
{{- define "test.1" -}}
|
||||||
|
{{ printf "Hello %s!" externalURL" }}
|
||||||
|
{{- end -}}
|
@ -0,0 +1,9 @@
|
|||||||
|
{{- define "test.1" -}}
|
||||||
|
{{ printf "Hello %s!" externalURL }}
|
||||||
|
{{- end -}}
|
||||||
|
{{- define "test.0" -}}
|
||||||
|
{{ printf "Hello %s!" externalURL }}
|
||||||
|
{{- end -}}
|
||||||
|
{{- define "test.3" -}}
|
||||||
|
{{ printf "Hello %s!" externalURL }}
|
||||||
|
{{- end -}}
|
9
app/vmalert/templates/templates/test/good0-test.tpl
Normal file
9
app/vmalert/templates/templates/test/good0-test.tpl
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{{- define "test.2" -}}
|
||||||
|
{{ printf "Hello %s!" externalURL }}
|
||||||
|
{{- end -}}
|
||||||
|
{{- define "test.0" -}}
|
||||||
|
{{ printf "Hello %s!" externalURL }}
|
||||||
|
{{- end -}}
|
||||||
|
{{- define "test.3" -}}
|
||||||
|
{{ printf "Hello %s!" externalURL }}
|
||||||
|
{{- end -}}
|
@ -25,6 +25,7 @@ implementation and aims to be compatible with its syntax.
|
|||||||
* Graphite datasource can be used for alerting and recording rules. See [these docs](#graphite);
|
* Graphite datasource can be used for alerting and recording rules. See [these docs](#graphite);
|
||||||
* Recording and Alerting rules backfilling (aka `replay`). See [these docs](#rules-backfilling);
|
* Recording and Alerting rules backfilling (aka `replay`). See [these docs](#rules-backfilling);
|
||||||
* Lightweight without extra dependencies.
|
* Lightweight without extra dependencies.
|
||||||
|
* Supports [reusable templates](#reusable-templates) for annotations.
|
||||||
|
|
||||||
## Limitations
|
## Limitations
|
||||||
|
|
||||||
@ -188,10 +189,53 @@ annotations:
|
|||||||
[ <labelname>: <tmpl_string> ]
|
[ <labelname>: <tmpl_string> ]
|
||||||
```
|
```
|
||||||
|
|
||||||
It is allowed to use [Go templating](https://golang.org/pkg/text/template/) in annotations
|
It is allowed to use [Go templating](https://golang.org/pkg/text/template/) in annotations to format data, iterate over it or execute expressions.
|
||||||
to format data, iterate over it or execute expressions.
|
|
||||||
Additionally, `vmalert` provides some extra templating functions
|
Additionally, `vmalert` provides some extra templating functions
|
||||||
listed [here](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/app/vmalert/notifier/template_func.go).
|
listed [here](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/app/vmalert/notifier/template_func.go) and [reusable templates](#reusable-templates).
|
||||||
|
|
||||||
|
#### Reusable templates
|
||||||
|
|
||||||
|
Like in Alertmanager you can use reusable templates to share same templates across anotations. Path to files with templates is provided with `-rule.templates` cli argument. E.g:
|
||||||
|
|
||||||
|
`/etc/vmalert/templates/global/common.tpl`
|
||||||
|
|
||||||
|
```
|
||||||
|
{{ define "grafana.filter" -}}
|
||||||
|
{{- $labels := .arg0 -}}
|
||||||
|
{{- range $name, $label := . -}}
|
||||||
|
{{- if (ne $name "arg0") -}}
|
||||||
|
{{- ( or (index $labels $label) "All" ) | printf "&var-%s=%s" $label -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
```
|
||||||
|
|
||||||
|
`/etc/vmalert/rules/project/rule.yaml`
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
groups:
|
||||||
|
- name: AlertGroupName
|
||||||
|
rules:
|
||||||
|
- alert: AlertName
|
||||||
|
expr: any_metric > 100
|
||||||
|
for: 30s
|
||||||
|
labels:
|
||||||
|
alertname: 'Any metric is too high'
|
||||||
|
severity: 'warning'
|
||||||
|
annotations:
|
||||||
|
dashboard: '{{ $externalURL }}/d/dashboard?orgId=1{{ template "grafana.filter" (args .CommonLabels "account_id" "any_label") }}'
|
||||||
|
```
|
||||||
|
|
||||||
|
`vmalert` configuration flags:
|
||||||
|
|
||||||
|
```
|
||||||
|
./bin/vmalert -rule=/etc/vmalert/rules/**/*.yaml \ # Path to the fules with rules configuration
|
||||||
|
-rule.templates=/etc/vmalert/templates/**/*.tpl \ # Path to the files with rule templates
|
||||||
|
-datasource.url=http://victoriametrics:8428 \ # VM-single addr for executing rules expressions
|
||||||
|
-remoteWrite.url=http://victoriametrics:8428 \ # VM-single addr to persist alerts state and recording rules results
|
||||||
|
-remoteRead.url=http://victoriametrics:8428 \ # VM-single addr for restoring alerts state after restart
|
||||||
|
-notifier.url=http://alertmanager:9093 # AlertManager addr to send alerts when they trigger
|
||||||
|
```
|
||||||
|
|
||||||
#### Recording rules
|
#### Recording rules
|
||||||
|
|
||||||
@ -796,6 +840,11 @@ The shortlist of configuration flags is the following:
|
|||||||
absolute path to all .yaml files in root.
|
absolute path to all .yaml files in root.
|
||||||
Rule files may contain %{ENV_VAR} placeholders, which are substituted by the corresponding env vars.
|
Rule files may contain %{ENV_VAR} placeholders, which are substituted by the corresponding env vars.
|
||||||
Supports an array of values separated by comma or specified via multiple flags.
|
Supports an array of values separated by comma or specified via multiple flags.
|
||||||
|
-rule.templates
|
||||||
|
Path or glob pattern to location with go template definitions for rules annotations templating. Flag can be specified multiple times.
|
||||||
|
Examples:
|
||||||
|
-rule.templates="/path/to/file". Path to a single file with go templates
|
||||||
|
-rule.templates="dir/*.tpl" -rule.templates="/*.tpl". Relative path to all .tpl files in "dir" folder, absolute path to all .tpl files in root.
|
||||||
-rule.configCheckInterval duration
|
-rule.configCheckInterval duration
|
||||||
Interval for checking for changes in '-rule' files. By default the checking is disabled. Send SIGHUP signal in order to force config check for changes. DEPRECATED - see '-configCheckInterval' instead
|
Interval for checking for changes in '-rule' files. By default the checking is disabled. Send SIGHUP signal in order to force config check for changes. DEPRECATED - see '-configCheckInterval' instead
|
||||||
-rule.maxResolveDuration duration
|
-rule.maxResolveDuration duration
|
||||||
|
Loading…
Reference in New Issue
Block a user