From 008b64965803b8bd22c320452f28af55643204c3 Mon Sep 17 00:00:00 2001 From: kirti purohit <58950467+Irene-123@users.noreply.github.com> Date: Tue, 8 Oct 2024 17:58:32 +0530 Subject: [PATCH] vmalert: parse multi doc yaml (#6995) ### Describe Your Changes This PR adds the feature to parse a multi yaml doc following the `\n---\n` The issue is [6753](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/6753) ### Checklist The following checks are **mandatory**: - [x] My change adheres [VictoriaMetrics contributing guidelines](https://docs.victoriametrics.com/contributing/). --------- Signed-off-by: kirti purohit Co-authored-by: kirti purohit Co-authored-by: Jiekun Co-authored-by: hagen1778 --- app/vmalert/config/config.go | 31 ++++++++--- app/vmalert/config/config_test.go | 53 ++++++++++++++++--- .../testdata/rules/rules-multi-doc-bad.rules | 29 ++++++++++ .../rules-multi-doc-duplicates-bad.rules | 11 ++++ .../testdata/rules/rules-multi-doc-good.rules | 15 ++++++ .../rules/rules-multi-doc2-good.rules | 46 ++++++++++++++++ app/vmalert/main.go | 4 +- docs/changelog/CHANGELOG.md | 1 + docs/vmalert.md | 4 +- 9 files changed, 175 insertions(+), 19 deletions(-) create mode 100644 app/vmalert/config/testdata/rules/rules-multi-doc-bad.rules create mode 100644 app/vmalert/config/testdata/rules/rules-multi-doc-duplicates-bad.rules create mode 100644 app/vmalert/config/testdata/rules/rules-multi-doc-good.rules create mode 100644 app/vmalert/config/testdata/rules/rules-multi-doc2-good.rules diff --git a/app/vmalert/config/config.go b/app/vmalert/config/config.go index 0e7ba7d6a5..31e16a1156 100644 --- a/app/vmalert/config/config.go +++ b/app/vmalert/config/config.go @@ -1,19 +1,20 @@ package config import ( + "bytes" "crypto/md5" "fmt" "hash/fnv" + "io" "net/url" "sort" "strings" - "gopkg.in/yaml.v2" - "github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/config/log" "github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/utils" "github.com/VictoriaMetrics/VictoriaMetrics/lib/envtemplate" "github.com/VictoriaMetrics/VictoriaMetrics/lib/promutils" + "gopkg.in/yaml.v2" ) // Group contains list of Rules grouped into @@ -298,16 +299,30 @@ func parseConfig(data []byte) ([]Group, error) { if err != nil { return nil, fmt.Errorf("cannot expand environment vars: %w", err) } - g := struct { + + var result []Group + type cfgFile struct { Groups []Group `yaml:"groups"` // Catches all undefined fields and must be empty after parsing. XXX map[string]any `yaml:",inline"` - }{} - err = yaml.Unmarshal(data, &g) - if err != nil { - return nil, err } - return g.Groups, checkOverflow(g.XXX, "config") + + decoder := yaml.NewDecoder(bytes.NewReader(data)) + for { + var cf cfgFile + if err = decoder.Decode(&cf); err != nil { + if err == io.EOF { // EOF indicates no more documents to read + break + } + return nil, err + } + if err = checkOverflow(cf.XXX, "config"); err != nil { + return nil, err + } + result = append(result, cf.Groups...) + } + + return result, nil } func checkOverflow(m map[string]any, ctx string) error { diff --git a/app/vmalert/config/config_test.go b/app/vmalert/config/config_test.go index 68184002c0..1aa06d582f 100644 --- a/app/vmalert/config/config_test.go +++ b/app/vmalert/config/config_test.go @@ -9,11 +9,10 @@ import ( "testing" "time" - "gopkg.in/yaml.v2" - "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) { @@ -40,6 +39,34 @@ groups: w.Write([]byte(` groups: - name: TestGroup + rules: + - record: conns + expr: max(vm_tcplistener_conns)`)) + }) + mux.HandleFunc("/good-multi-doc", func(w http.ResponseWriter, _ *http.Request) { + w.Write([]byte(` +groups: + - name: foo + rules: + - record: conns + expr: max(vm_tcplistener_conns) +--- +groups: + - name: bar + rules: + - record: conns + expr: max(vm_tcplistener_conns)`)) + }) + mux.HandleFunc("/bad-multi-doc", func(w http.ResponseWriter, _ *http.Request) { + w.Write([]byte(` +bad_field: + - name: foo + rules: + - record: conns + expr: max(vm_tcplistener_conns) +--- +groups: + - name: bar rules: - record: conns expr: max(vm_tcplistener_conns)`)) @@ -48,13 +75,23 @@ groups: srv := httptest.NewServer(mux) defer srv.Close() - if _, err := Parse([]string{srv.URL + "/good-alert", srv.URL + "/good-rr"}, notifier.ValidateTemplates, true); err != nil { - t.Fatalf("error parsing URLs %s", err) + f := func(urls []string, expErr bool) { + for i, u := range urls { + urls[i] = srv.URL + u + } + _, err := Parse(urls, notifier.ValidateTemplates, true) + if err != nil && !expErr { + t.Fatalf("error parsing URLs %s", err) + } + if err == nil && expErr { + t.Fatalf("expecting error parsing URLs but got none") + } } - if _, err := Parse([]string{srv.URL + "/bad"}, notifier.ValidateTemplates, true); err == nil { - t.Fatalf("expected parsing error: %s", err) - } + f([]string{"/good-alert", "/good-rr", "/good-multi-doc"}, false) + f([]string{"/bad"}, true) + f([]string{"/bad-multi-doc"}, true) + f([]string{"/good-alert", "/bad"}, true) } func TestParse_Success(t *testing.T) { @@ -86,6 +123,8 @@ func TestParse_Failure(t *testing.T) { f([]string{"testdata/dir/rules4-bad.rules"}, "either `record` or `alert` must be set") f([]string{"testdata/rules/rules1-bad.rules"}, "bad graphite expr") f([]string{"testdata/dir/rules6-bad.rules"}, "missing ':' in header") + f([]string{"testdata/rules/rules-multi-doc-bad.rules"}, "unknown fields") + f([]string{"testdata/rules/rules-multi-doc-duplicates-bad.rules"}, "duplicate") f([]string{"http://unreachable-url"}, "failed to") } diff --git a/app/vmalert/config/testdata/rules/rules-multi-doc-bad.rules b/app/vmalert/config/testdata/rules/rules-multi-doc-bad.rules new file mode 100644 index 0000000000..191f733608 --- /dev/null +++ b/app/vmalert/config/testdata/rules/rules-multi-doc-bad.rules @@ -0,0 +1,29 @@ +groups: + - name: groupTest + rules: + - alert: VMRows + for: 1ms + expr: vm_rows > 0 + labels: + label: bar + host: "{{ $labels.instance }}" + annotations: + summary: "{{ $value }}" +invalid-field-1: invalid-value-1 +invalid-field-2: invalid-value-2 +--- +groups: + - name: TestGroup + interval: 2s + concurrency: 2 + type: graphite + rules: + - alert: Conns + expr: filterSeries(sumSeries(host.receiver.interface.cons),'last','>', 500) + for: 3m + + annotations: + summary: Too high connection number for {{$labels.instance}} + description: "It is {{ $value }} connections for {{$labels.instance}}" +invalid-field-2: invalid-value-2 +invalid-field-3: invalid-value-3 \ No newline at end of file diff --git a/app/vmalert/config/testdata/rules/rules-multi-doc-duplicates-bad.rules b/app/vmalert/config/testdata/rules/rules-multi-doc-duplicates-bad.rules new file mode 100644 index 0000000000..17bc4fda30 --- /dev/null +++ b/app/vmalert/config/testdata/rules/rules-multi-doc-duplicates-bad.rules @@ -0,0 +1,11 @@ +groups: + - name: foo + rules: + - alert: VMRows + expr: vm_rows > 0 +--- +groups: + - name: foo + rules: + - alert: VMRows + expr: vm_rows > 0 \ No newline at end of file diff --git a/app/vmalert/config/testdata/rules/rules-multi-doc-good.rules b/app/vmalert/config/testdata/rules/rules-multi-doc-good.rules new file mode 100644 index 0000000000..03db35a7e5 --- /dev/null +++ b/app/vmalert/config/testdata/rules/rules-multi-doc-good.rules @@ -0,0 +1,15 @@ + +--- +groups: + - name: groupTest + rules: + - alert: VMRows + for: 1ms + expr: vm_rows > 0 + labels: + label: bar + host: "{{ $labels.instance }}" + annotations: + summary: "{{ $value }}" +--- +groups: \ No newline at end of file diff --git a/app/vmalert/config/testdata/rules/rules-multi-doc2-good.rules b/app/vmalert/config/testdata/rules/rules-multi-doc2-good.rules new file mode 100644 index 0000000000..1115f26552 --- /dev/null +++ b/app/vmalert/config/testdata/rules/rules-multi-doc2-good.rules @@ -0,0 +1,46 @@ +--- +groups: + - name: groupTest + rules: + - alert: VMRows + for: 1ms + expr: vm_rows > 0 + labels: + label: bar + host: "{{ $labels.instance }}" + annotations: + summary: "{{ $value }}" + - name: groupTest-2 + rules: + - alert: VMRows-2 + for: 1ms + expr: vm_rows_2 > 0 + labels: + label: bar2 + host: "{{ $labels.instance }}" + annotations: + summary: "\n markdown result is : \n---\n # header\n body: \n text \n----\n" +--- +groups: + - name: groupTest-3 + rules: + - alert: VMRows-3 + for: 1ms + expr: vm_rows_3 > 0 + labels: + label: bar_3 + host: "{{ $labels.instance }}" + annotations: + summary: "{{ $value }}" + - name: groupTest-4 + rules: + - alert: VMRows-4 + for: 1ms + expr: vm_rows_4 > 0 + labels: + label: bar4 + host: "{{ $labels.instance }}" + annotations: + summary: "{{ $value }}" +--- +groups: \ No newline at end of file diff --git a/app/vmalert/main.go b/app/vmalert/main.go index 1e9e6f2eae..53af8f210c 100644 --- a/app/vmalert/main.go +++ b/app/vmalert/main.go @@ -31,14 +31,14 @@ import ( ) var ( - rulePath = flagutil.NewArrayString("rule", `Path to the files or http url with alerting and/or recording rules. + rulePath = flagutil.NewArrayString("rule", `Path to the files or http url with alerting and/or recording rules in YAML format. Supports hierarchical patterns and regexpes. Examples: -rule="/path/to/file". Path to a single file with alerting rules. -rule="http:///path/to/rules". HTTP URL to a page with alerting rules. -rule="dir/*.yaml" -rule="/*.yaml" -rule="gcs://vmalert-rules/tenant_%{TENANT_ID}/prod". -rule="dir/**/*.yaml". Includes all the .yaml files in "dir" subfolders recursively. -Rule files may contain %{ENV_VAR} placeholders, which are substituted by the corresponding env vars. +Rule files support YAML multi-document. Files may contain %{ENV_VAR} placeholders, which are substituted by the corresponding env vars. Enterprise version of vmalert supports S3 and GCS paths to rules. For example: gs://bucket/path/to/rules, s3://bucket/path/to/rules diff --git a/docs/changelog/CHANGELOG.md b/docs/changelog/CHANGELOG.md index 05ae453add..2d412503ee 100644 --- a/docs/changelog/CHANGELOG.md +++ b/docs/changelog/CHANGELOG.md @@ -20,6 +20,7 @@ See also [LTS releases](https://docs.victoriametrics.com/lts-releases/). * FEATURE: add Darwin binaries for [VictoriaMetrics cluster](https://docs.victoriametrics.com/cluster-victoriametrics/) to the release flow. The binaries will be available in the new release. * FEATURE: [vmagent](https://docs.victoriametrics.com/vmagent/): allow using HTTP/2 client for Kubernetes service discovery if `-promscrape.kubernetes.useHTTP2Client` cmd-line flag is set. This could help to reduce the amount of opened connections to the Kubernetes API server. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/5971) for the details. +* FEATURE: [vmalert](https://docs.victoriametrics.com/vmalert/): `-rule` cmd-line flag now supports multi-document YAML files. This could be usefule when rules are retrieved from via HTTP where multiple rule files were merged together in one response. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/6753). Thanks to @Irene-123 for [the pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/6995). ## [v1.104.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.104.0) diff --git a/docs/vmalert.md b/docs/vmalert.md index 2dbf99083c..61a2e6ae37 100644 --- a/docs/vmalert.md +++ b/docs/vmalert.md @@ -1427,14 +1427,14 @@ The shortlist of configuration flags is the following: -replay.timeTo string The time filter in RFC3339 format to finish the replay by. E.g. '2020-01-01T20:07:00Z'. By default, is set to the current time. -rule array - Path to the files or http url with alerting and/or recording rules. + Path to the files or http url with alerting and/or recording rules in YAML format. Supports hierarchical patterns and regexpes. Examples: -rule="/path/to/file". Path to a single file with alerting rules. -rule="http:///path/to/rules". HTTP URL to a page with alerting rules. -rule="dir/*.yaml" -rule="/*.yaml" -rule="gcs://vmalert-rules/tenant_%{TENANT_ID}/prod". -rule="dir/**/*.yaml". Includes all the .yaml files in "dir" subfolders recursively. - Rule files may contain %{ENV_VAR} placeholders, which are substituted by the corresponding env vars. + Rule files support YAML multi-document. Files may contain %{ENV_VAR} placeholders, which are substituted by the corresponding env vars. Enterprise version of vmalert supports S3 and GCS paths to rules. For example: gs://bucket/path/to/rules, s3://bucket/path/to/rules S3 and GCS paths support only matching by prefix, e.g. s3://bucket/dir/rule_ matches