2020-06-01 10:46:37 +00:00
|
|
|
package config
|
|
|
|
|
|
|
|
import (
|
2020-09-11 19:14:30 +00:00
|
|
|
"crypto/md5"
|
2020-06-01 10:46:37 +00:00
|
|
|
"fmt"
|
2020-06-15 19:15:47 +00:00
|
|
|
"hash/fnv"
|
2021-12-02 12:45:08 +00:00
|
|
|
"net/url"
|
2020-06-15 19:15:47 +00:00
|
|
|
"sort"
|
2020-06-01 10:46:37 +00:00
|
|
|
"strings"
|
|
|
|
|
2021-12-02 12:45:08 +00:00
|
|
|
"gopkg.in/yaml.v2"
|
2021-02-01 13:02:44 +00:00
|
|
|
|
2023-03-20 15:08:30 +00:00
|
|
|
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/config/log"
|
2020-10-20 07:15:21 +00:00
|
|
|
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/utils"
|
2020-08-13 13:43:55 +00:00
|
|
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/envtemplate"
|
2022-02-11 14:17:00 +00:00
|
|
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promutils"
|
2020-06-01 10:46:37 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// Group contains list of Rules grouped into
|
|
|
|
// entity with one name and evaluation interval
|
|
|
|
type Group struct {
|
2022-07-22 08:44:55 +00:00
|
|
|
Type Type `yaml:"type,omitempty"`
|
2020-06-09 12:21:20 +00:00
|
|
|
File string
|
2022-04-16 11:25:54 +00:00
|
|
|
Name string `yaml:"name"`
|
|
|
|
Interval *promutils.Duration `yaml:"interval,omitempty"`
|
2022-06-09 06:21:30 +00:00
|
|
|
Limit int `yaml:"limit,omitempty"`
|
2022-04-16 11:25:54 +00:00
|
|
|
Rules []Rule `yaml:"rules"`
|
|
|
|
Concurrency int `yaml:"concurrency"`
|
2021-08-31 11:52:34 +00:00
|
|
|
// Labels is a set of label value pairs, that will be added to every rule.
|
|
|
|
// It has priority over the external labels.
|
|
|
|
Labels map[string]string `yaml:"labels"`
|
2020-09-11 19:14:30 +00:00
|
|
|
// Checksum stores the hash of yaml definition for this group.
|
|
|
|
// May be used to detect any changes like rules re-ordering etc.
|
|
|
|
Checksum string
|
2021-12-02 12:45:08 +00:00
|
|
|
// Optional HTTP URL parameters added to each rule request
|
|
|
|
Params url.Values `yaml:"params"`
|
2022-07-21 13:59:55 +00:00
|
|
|
// Headers contains optional HTTP headers added to each rule request
|
2022-07-22 08:44:55 +00:00
|
|
|
Headers []Header `yaml:"headers,omitempty"`
|
2023-04-27 11:02:21 +00:00
|
|
|
// NotifierHeaders contains optional HTTP headers sent to notifiers for generated notifications
|
2023-04-27 10:17:26 +00:00
|
|
|
NotifierHeaders []Header `yaml:"notifier_headers,omitempty"`
|
2020-06-01 10:46:37 +00:00
|
|
|
// Catches all undefined fields and must be empty after parsing.
|
|
|
|
XXX map[string]interface{} `yaml:",inline"`
|
|
|
|
}
|
|
|
|
|
2020-09-11 19:14:30 +00:00
|
|
|
// UnmarshalYAML implements the yaml.Unmarshaler interface.
|
|
|
|
func (g *Group) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
|
|
|
type group Group
|
|
|
|
if err := unmarshal((*group)(g)); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
b, err := yaml.Marshal(g)
|
|
|
|
if err != nil {
|
2020-09-23 19:46:24 +00:00
|
|
|
return fmt.Errorf("failed to marshal group configuration for checksum: %w", err)
|
2020-09-11 19:14:30 +00:00
|
|
|
}
|
2021-02-01 13:02:44 +00:00
|
|
|
// change default value to prometheus datasource.
|
|
|
|
if g.Type.Get() == "" {
|
2022-07-22 08:44:55 +00:00
|
|
|
g.Type.Set(NewPrometheusType())
|
2021-02-01 13:02:44 +00:00
|
|
|
}
|
|
|
|
|
2020-09-11 19:14:30 +00:00
|
|
|
h := md5.New()
|
|
|
|
h.Write(b)
|
|
|
|
g.Checksum = fmt.Sprintf("%x", h.Sum(nil))
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-06-01 10:46:37 +00:00
|
|
|
// Validate check for internal Group or Rule configuration errors
|
2022-07-22 11:50:41 +00:00
|
|
|
func (g *Group) Validate(validateTplFn ValidateTplFn, validateExpressions bool) error {
|
2020-06-01 10:46:37 +00:00
|
|
|
if g.Name == "" {
|
|
|
|
return fmt.Errorf("group name must be set")
|
|
|
|
}
|
2021-02-01 13:02:44 +00:00
|
|
|
|
2020-06-15 19:15:47 +00:00
|
|
|
uniqueRules := map[uint64]struct{}{}
|
2020-06-01 10:46:37 +00:00
|
|
|
for _, r := range g.Rules {
|
|
|
|
ruleName := r.Record
|
|
|
|
if r.Alert != "" {
|
|
|
|
ruleName = r.Alert
|
|
|
|
}
|
2020-06-15 19:15:47 +00:00
|
|
|
if _, ok := uniqueRules[r.ID]; ok {
|
2022-09-20 10:52:46 +00:00
|
|
|
return fmt.Errorf("%q is a duplicate within the group %q", r.String(), g.Name)
|
2020-06-01 10:46:37 +00:00
|
|
|
}
|
2020-06-15 19:15:47 +00:00
|
|
|
uniqueRules[r.ID] = struct{}{}
|
2020-06-01 10:46:37 +00:00
|
|
|
if err := r.Validate(); err != nil {
|
2020-06-30 19:58:18 +00:00
|
|
|
return fmt.Errorf("invalid rule %q.%q: %w", g.Name, ruleName, err)
|
2020-06-01 10:46:37 +00:00
|
|
|
}
|
2020-06-06 20:27:09 +00:00
|
|
|
if validateExpressions {
|
2021-02-01 13:02:44 +00:00
|
|
|
// its needed only for tests.
|
|
|
|
// because correct types must be inherited after unmarshalling.
|
|
|
|
exprValidator := g.Type.ValidateExpr
|
|
|
|
if err := exprValidator(r.Expr); err != nil {
|
2020-06-30 19:58:18 +00:00
|
|
|
return fmt.Errorf("invalid expression for rule %q.%q: %w", g.Name, ruleName, err)
|
2020-06-06 20:27:09 +00:00
|
|
|
}
|
2020-06-01 10:46:37 +00:00
|
|
|
}
|
2022-07-22 11:50:41 +00:00
|
|
|
if validateTplFn != nil {
|
|
|
|
if err := validateTplFn(r.Annotations); err != nil {
|
2020-06-30 19:58:18 +00:00
|
|
|
return fmt.Errorf("invalid annotations for rule %q.%q: %w", g.Name, ruleName, err)
|
2020-06-06 20:27:09 +00:00
|
|
|
}
|
2022-07-22 11:50:41 +00:00
|
|
|
if err := validateTplFn(r.Labels); err != nil {
|
2020-06-30 19:58:18 +00:00
|
|
|
return fmt.Errorf("invalid labels for rule %q.%q: %w", g.Name, ruleName, err)
|
2020-06-06 20:27:09 +00:00
|
|
|
}
|
2020-06-01 10:46:37 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return checkOverflow(g.XXX, fmt.Sprintf("group %q", g.Name))
|
|
|
|
}
|
|
|
|
|
|
|
|
// Rule describes entity that represent either
|
|
|
|
// recording rule or alerting rule.
|
|
|
|
type Rule struct {
|
2020-06-15 19:15:47 +00:00
|
|
|
ID uint64
|
2022-04-16 11:25:54 +00:00
|
|
|
Record string `yaml:"record,omitempty"`
|
|
|
|
Alert string `yaml:"alert,omitempty"`
|
|
|
|
Expr string `yaml:"expr"`
|
|
|
|
For *promutils.Duration `yaml:"for,omitempty"`
|
|
|
|
Labels map[string]string `yaml:"labels,omitempty"`
|
|
|
|
Annotations map[string]string `yaml:"annotations,omitempty"`
|
2022-09-13 13:25:43 +00:00
|
|
|
Debug bool `yaml:"debug,omitempty"`
|
2022-12-29 11:36:44 +00:00
|
|
|
// UpdateEntriesLimit defines max number of rule's state updates stored in memory.
|
|
|
|
// Overrides `-rule.updateEntriesLimit`.
|
|
|
|
UpdateEntriesLimit *int `yaml:"update_entries_limit,omitempty"`
|
2020-06-15 19:15:47 +00:00
|
|
|
|
|
|
|
// Catches all undefined fields and must be empty after parsing.
|
|
|
|
XXX map[string]interface{} `yaml:",inline"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// UnmarshalYAML implements the yaml.Unmarshaler interface.
|
|
|
|
func (r *Rule) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
|
|
|
type rule Rule
|
|
|
|
if err := unmarshal((*rule)(r)); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
r.ID = HashRule(*r)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-07-28 11:20:31 +00:00
|
|
|
// Name returns Rule name according to its type
|
|
|
|
func (r *Rule) Name() string {
|
|
|
|
if r.Record != "" {
|
|
|
|
return r.Record
|
|
|
|
}
|
|
|
|
return r.Alert
|
|
|
|
}
|
|
|
|
|
2022-09-20 10:52:46 +00:00
|
|
|
// String implements Stringer interface
|
|
|
|
func (r *Rule) String() string {
|
|
|
|
ruleType := "recording"
|
|
|
|
if r.Alert != "" {
|
|
|
|
ruleType = "alerting"
|
|
|
|
}
|
|
|
|
b := strings.Builder{}
|
|
|
|
b.WriteString(fmt.Sprintf("%s rule %q", ruleType, r.Name()))
|
|
|
|
b.WriteString(fmt.Sprintf("; expr: %q", r.Expr))
|
|
|
|
|
|
|
|
kv := sortMap(r.Labels)
|
|
|
|
for i := range kv {
|
|
|
|
if i == 0 {
|
|
|
|
b.WriteString("; labels:")
|
|
|
|
}
|
|
|
|
b.WriteString(" ")
|
|
|
|
b.WriteString(kv[i].key)
|
|
|
|
b.WriteString("=")
|
|
|
|
b.WriteString(kv[i].value)
|
|
|
|
if i < len(kv)-1 {
|
|
|
|
b.WriteString(",")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return b.String()
|
|
|
|
}
|
|
|
|
|
2020-06-15 19:15:47 +00:00
|
|
|
// HashRule hashes significant Rule fields into
|
2020-09-11 19:14:30 +00:00
|
|
|
// unique hash that supposed to define Rule uniqueness
|
2020-06-15 19:15:47 +00:00
|
|
|
func HashRule(r Rule) uint64 {
|
|
|
|
h := fnv.New64a()
|
|
|
|
h.Write([]byte(r.Expr))
|
|
|
|
if r.Record != "" {
|
|
|
|
h.Write([]byte("recording"))
|
|
|
|
h.Write([]byte(r.Record))
|
|
|
|
} else {
|
|
|
|
h.Write([]byte("alerting"))
|
|
|
|
h.Write([]byte(r.Alert))
|
|
|
|
}
|
2020-09-11 19:14:30 +00:00
|
|
|
kv := sortMap(r.Labels)
|
2020-06-15 19:15:47 +00:00
|
|
|
for _, i := range kv {
|
|
|
|
h.Write([]byte(i.key))
|
|
|
|
h.Write([]byte(i.value))
|
|
|
|
h.Write([]byte("\xff"))
|
|
|
|
}
|
|
|
|
return h.Sum64()
|
2020-06-01 10:46:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Validate check for Rule configuration errors
|
|
|
|
func (r *Rule) Validate() error {
|
|
|
|
if (r.Record == "" && r.Alert == "") || (r.Record != "" && r.Alert != "") {
|
|
|
|
return fmt.Errorf("either `record` or `alert` must be set")
|
|
|
|
}
|
|
|
|
if r.Expr == "" {
|
|
|
|
return fmt.Errorf("expression can't be empty")
|
|
|
|
}
|
2020-06-15 19:15:47 +00:00
|
|
|
return checkOverflow(r.XXX, "rule")
|
2020-06-01 10:46:37 +00:00
|
|
|
}
|
|
|
|
|
2022-07-25 06:22:09 +00:00
|
|
|
// ValidateTplFn must validate the given annotations
|
2022-07-22 11:50:41 +00:00
|
|
|
type ValidateTplFn func(annotations map[string]string) error
|
|
|
|
|
2023-03-20 15:08:30 +00:00
|
|
|
// cLogger is a logger with support of logs suppressing.
|
|
|
|
// it is used when logs emitted by config package needs
|
|
|
|
// to be suppressed.
|
|
|
|
var cLogger = &log.Logger{}
|
|
|
|
|
2023-03-09 13:46:19 +00:00
|
|
|
// ParseSilent parses rule configs from given file patterns without emitting logs
|
|
|
|
func ParseSilent(pathPatterns []string, validateTplFn ValidateTplFn, validateExpressions bool) ([]Group, error) {
|
2023-03-20 15:08:30 +00:00
|
|
|
cLogger.Suppress(true)
|
|
|
|
defer cLogger.Suppress(false)
|
2023-05-08 07:52:57 +00:00
|
|
|
files, err := readFromFSOrHTTP(pathPatterns)
|
2023-03-09 13:46:19 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to read from the config: %s", err)
|
|
|
|
}
|
|
|
|
return parse(files, validateTplFn, validateExpressions)
|
|
|
|
}
|
|
|
|
|
2020-06-01 10:46:37 +00:00
|
|
|
// Parse parses rule configs from given file patterns
|
2022-07-22 11:50:41 +00:00
|
|
|
func Parse(pathPatterns []string, validateTplFn ValidateTplFn, validateExpressions bool) ([]Group, error) {
|
2023-05-08 07:52:57 +00:00
|
|
|
files, err := readFromFSOrHTTP(pathPatterns)
|
2023-02-10 01:18:27 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to read from the config: %s", err)
|
2020-06-01 10:46:37 +00:00
|
|
|
}
|
2023-03-09 13:46:19 +00:00
|
|
|
groups, err := parse(files, validateTplFn, validateExpressions)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to parse %s: %s", pathPatterns, err)
|
|
|
|
}
|
|
|
|
if len(groups) < 1 {
|
2023-03-20 15:08:30 +00:00
|
|
|
cLogger.Warnf("no groups found in %s", strings.Join(pathPatterns, ";"))
|
2023-03-09 13:46:19 +00:00
|
|
|
}
|
|
|
|
return groups, nil
|
2023-05-08 07:52:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// readFromFSOrHTTP reads path either from filesystem or from http if path starts with http or https.
|
|
|
|
func readFromFSOrHTTP(paths []string) (map[string][]byte, error) {
|
|
|
|
var httpPaths []string
|
|
|
|
var fsPaths []string
|
|
|
|
for _, path := range paths {
|
|
|
|
if isHTTPURL(path) {
|
|
|
|
httpPaths = append(httpPaths, path)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
fsPaths = append(fsPaths, path)
|
|
|
|
}
|
|
|
|
result, err := readFromFS(fsPaths)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
httpResult, err := readFromHTTP(httpPaths)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
for k, v := range httpResult {
|
|
|
|
if _, ok := result[k]; ok {
|
|
|
|
return nil, fmt.Errorf("duplicate found for config name %q: config names must be unique", k)
|
|
|
|
}
|
|
|
|
result[k] = v
|
|
|
|
}
|
|
|
|
return result, nil
|
2023-03-09 13:46:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func parse(files map[string][]byte, validateTplFn ValidateTplFn, validateExpressions bool) ([]Group, error) {
|
2020-10-20 07:15:21 +00:00
|
|
|
errGroup := new(utils.ErrGroup)
|
2020-06-01 10:46:37 +00:00
|
|
|
var groups []Group
|
2023-02-10 01:18:27 +00:00
|
|
|
for file, data := range files {
|
2020-06-01 10:46:37 +00:00
|
|
|
uniqueGroups := map[string]struct{}{}
|
2023-02-10 01:18:27 +00:00
|
|
|
gr, err := parseConfig(data)
|
2020-06-01 10:46:37 +00:00
|
|
|
if err != nil {
|
2020-10-20 07:15:21 +00:00
|
|
|
errGroup.Add(fmt.Errorf("failed to parse file %q: %w", file, err))
|
|
|
|
continue
|
2020-06-01 10:46:37 +00:00
|
|
|
}
|
|
|
|
for _, g := range gr {
|
2022-07-22 11:50:41 +00:00
|
|
|
if err := g.Validate(validateTplFn, validateExpressions); err != nil {
|
2020-10-20 07:15:21 +00:00
|
|
|
errGroup.Add(fmt.Errorf("invalid group %q in file %q: %w", g.Name, file, err))
|
|
|
|
continue
|
2020-06-01 10:46:37 +00:00
|
|
|
}
|
|
|
|
if _, ok := uniqueGroups[g.Name]; ok {
|
2020-10-20 07:15:21 +00:00
|
|
|
errGroup.Add(fmt.Errorf("group name %q duplicate in file %q", g.Name, file))
|
|
|
|
continue
|
2020-06-01 10:46:37 +00:00
|
|
|
}
|
|
|
|
uniqueGroups[g.Name] = struct{}{}
|
|
|
|
g.File = file
|
|
|
|
groups = append(groups, g)
|
|
|
|
}
|
|
|
|
}
|
2020-10-20 07:15:21 +00:00
|
|
|
if err := errGroup.Err(); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2023-03-09 13:46:19 +00:00
|
|
|
sort.SliceStable(groups, func(i, j int) bool {
|
|
|
|
if groups[i].File != groups[j].File {
|
|
|
|
return groups[i].File < groups[j].File
|
|
|
|
}
|
|
|
|
return groups[i].Name < groups[j].Name
|
|
|
|
})
|
2020-06-01 10:46:37 +00:00
|
|
|
return groups, nil
|
|
|
|
}
|
|
|
|
|
2023-02-10 01:18:27 +00:00
|
|
|
func parseConfig(data []byte) ([]Group, error) {
|
|
|
|
data, err := envtemplate.ReplaceBytes(data)
|
2022-10-18 07:28:39 +00:00
|
|
|
if err != nil {
|
2023-02-10 01:18:27 +00:00
|
|
|
return nil, fmt.Errorf("cannot expand environment vars: %w", err)
|
2020-06-01 10:46:37 +00:00
|
|
|
}
|
|
|
|
g := struct {
|
|
|
|
Groups []Group `yaml:"groups"`
|
|
|
|
// Catches all undefined fields and must be empty after parsing.
|
|
|
|
XXX map[string]interface{} `yaml:",inline"`
|
|
|
|
}{}
|
|
|
|
err = yaml.Unmarshal(data, &g)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return g.Groups, checkOverflow(g.XXX, "config")
|
|
|
|
}
|
|
|
|
|
|
|
|
func checkOverflow(m map[string]interface{}, ctx string) error {
|
|
|
|
if len(m) > 0 {
|
|
|
|
var keys []string
|
|
|
|
for k := range m {
|
|
|
|
keys = append(keys, k)
|
|
|
|
}
|
|
|
|
return fmt.Errorf("unknown fields in %s: %s", ctx, strings.Join(keys, ", "))
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
2020-09-11 19:14:30 +00:00
|
|
|
|
|
|
|
type item struct {
|
|
|
|
key, value string
|
|
|
|
}
|
|
|
|
|
|
|
|
func sortMap(m map[string]string) []item {
|
|
|
|
var kv []item
|
|
|
|
for k, v := range m {
|
|
|
|
kv = append(kv, item{key: k, value: v})
|
|
|
|
}
|
|
|
|
sort.Slice(kv, func(i, j int) bool {
|
|
|
|
return kv[i].key < kv[j].key
|
|
|
|
})
|
|
|
|
return kv
|
|
|
|
}
|