mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2024-11-23 20:37:12 +01:00
008b649658
Some checks are pending
build / Build (push) Waiting to run
CodeQL Go / Analyze (push) Waiting to run
main / lint (push) Waiting to run
main / test (test-full) (push) Blocked by required conditions
main / test (test-full-386) (push) Blocked by required conditions
main / test (test-pure) (push) Blocked by required conditions
publish-docs / Build (push) Waiting to run
### 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 <kirti.purohit@hpe.com> Co-authored-by: kirti purohit <kirti.purohit@hpe.com> Co-authored-by: Jiekun <jiekun@victoriametrics.com> Co-authored-by: hagen1778 <roman@victoriametrics.com>
656 lines
15 KiB
Go
656 lines
15 KiB
Go
package config
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"os"
|
|
"strings"
|
|
"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"}, true); err != nil {
|
|
os.Exit(1)
|
|
}
|
|
os.Exit(m.Run())
|
|
}
|
|
|
|
func TestParseFromURL(t *testing.T) {
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/bad", func(w http.ResponseWriter, _ *http.Request) {
|
|
w.Write([]byte("foo bar"))
|
|
})
|
|
mux.HandleFunc("/good-alert", func(w http.ResponseWriter, _ *http.Request) {
|
|
w.Write([]byte(`
|
|
groups:
|
|
- name: TestGroup
|
|
rules:
|
|
- alert: Conns
|
|
expr: vm_tcplistener_conns > 0`))
|
|
})
|
|
mux.HandleFunc("/good-rr", func(w http.ResponseWriter, _ *http.Request) {
|
|
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)`))
|
|
})
|
|
|
|
srv := httptest.NewServer(mux)
|
|
defer srv.Close()
|
|
|
|
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")
|
|
}
|
|
}
|
|
|
|
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) {
|
|
_, err := Parse([]string{"testdata/rules/*good.rules", "testdata/dir/*good.*"}, notifier.ValidateTemplates, true)
|
|
if err != nil {
|
|
t.Fatalf("error parsing files %s", err)
|
|
}
|
|
}
|
|
|
|
func TestParse_Failure(t *testing.T) {
|
|
f := func(paths []string, errStrExpected string) {
|
|
t.Helper()
|
|
|
|
_, err := Parse(paths, notifier.ValidateTemplates, true)
|
|
if err == nil {
|
|
t.Fatalf("expected to get error")
|
|
}
|
|
if !strings.Contains(err.Error(), errStrExpected) {
|
|
t.Fatalf("expected err to contain %q; got %q instead", errStrExpected, err)
|
|
}
|
|
}
|
|
|
|
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/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")
|
|
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")
|
|
}
|
|
|
|
func TestRuleValidate(t *testing.T) {
|
|
if err := (&Rule{}).Validate(); err == nil {
|
|
t.Fatalf("expected empty name error")
|
|
}
|
|
if err := (&Rule{Alert: "alert"}).Validate(); err == nil {
|
|
t.Fatalf("expected empty expr error")
|
|
}
|
|
if err := (&Rule{Alert: "alert", Expr: "test>0"}).Validate(); err != nil {
|
|
t.Fatalf("expected valid rule; got %s", err)
|
|
}
|
|
}
|
|
|
|
func TestGroupValidate_Failure(t *testing.T) {
|
|
f := func(group *Group, validateExpressions bool, errStrExpected string) {
|
|
t.Helper()
|
|
|
|
err := group.Validate(nil, validateExpressions)
|
|
if err == nil {
|
|
t.Fatalf("expecting non-nil error")
|
|
}
|
|
errStr := err.Error()
|
|
if !strings.Contains(errStr, errStrExpected) {
|
|
t.Fatalf("missing %q in the returned error %q", errStrExpected, errStr)
|
|
}
|
|
}
|
|
|
|
f(&Group{}, false, "group name must be set")
|
|
|
|
f(&Group{
|
|
Name: "negative interval",
|
|
Interval: promutils.NewDuration(-1),
|
|
}, false, "interval shouldn't be lower than 0")
|
|
|
|
f(&Group{
|
|
Name: "wrong eval_offset",
|
|
Interval: promutils.NewDuration(time.Minute),
|
|
EvalOffset: promutils.NewDuration(2 * time.Minute),
|
|
}, false, "eval_offset should be smaller than interval")
|
|
|
|
f(&Group{
|
|
Name: "wrong limit",
|
|
Limit: -1,
|
|
}, false, "invalid limit")
|
|
|
|
f(&Group{
|
|
Name: "wrong concurrency",
|
|
Concurrency: -1,
|
|
}, false, "invalid concurrency")
|
|
|
|
f(&Group{
|
|
Name: "test",
|
|
Rules: []Rule{
|
|
{
|
|
Alert: "alert",
|
|
Expr: "up == 1",
|
|
},
|
|
{
|
|
Alert: "alert",
|
|
Expr: "up == 1",
|
|
},
|
|
},
|
|
}, false, "duplicate")
|
|
|
|
f(&Group{
|
|
Name: "test",
|
|
Rules: []Rule{
|
|
{Alert: "alert", Expr: "up == 1", Labels: map[string]string{
|
|
"summary": "{{ value|query }}",
|
|
}},
|
|
{Alert: "alert", Expr: "up == 1", Labels: map[string]string{
|
|
"summary": "{{ value|query }}",
|
|
}},
|
|
},
|
|
}, false, "duplicate")
|
|
|
|
f(&Group{
|
|
Name: "test",
|
|
Rules: []Rule{
|
|
{Record: "record", Expr: "up == 1", Labels: map[string]string{
|
|
"summary": "{{ value|query }}",
|
|
}},
|
|
{Record: "record", Expr: "up == 1", Labels: map[string]string{
|
|
"summary": "{{ value|query }}",
|
|
}},
|
|
},
|
|
}, false, "duplicate")
|
|
|
|
f(&Group{
|
|
Name: "test",
|
|
Rules: []Rule{
|
|
{Alert: "alert", Expr: "up == 1", Labels: map[string]string{
|
|
"summary": "{{ value|query }}",
|
|
}},
|
|
{Alert: "alert", Expr: "up == 1", Labels: map[string]string{
|
|
"description": "{{ value|query }}",
|
|
}},
|
|
},
|
|
}, false, "duplicate")
|
|
|
|
f(&Group{
|
|
Name: "test",
|
|
Rules: []Rule{
|
|
{Record: "alert", Expr: "up == 1", Labels: map[string]string{
|
|
"summary": "{{ value|query }}",
|
|
}},
|
|
{Alert: "alert", Expr: "up == 1", Labels: map[string]string{
|
|
"summary": "{{ value|query }}",
|
|
}},
|
|
},
|
|
}, false, "duplicate")
|
|
|
|
f(&Group{
|
|
Name: "test graphite prometheus bad expr",
|
|
Type: NewGraphiteType(),
|
|
Rules: []Rule{
|
|
{
|
|
Expr: "sum(up == 0 ) by (host)",
|
|
For: promutils.NewDuration(10 * time.Millisecond),
|
|
},
|
|
{
|
|
Expr: "sumSeries(time('foo.bar',10))",
|
|
},
|
|
},
|
|
}, false, "invalid rule")
|
|
|
|
f(&Group{
|
|
Name: "test graphite inherit",
|
|
Type: NewGraphiteType(),
|
|
Rules: []Rule{
|
|
{
|
|
Expr: "sumSeries(time('foo.bar',10))",
|
|
For: promutils.NewDuration(10 * time.Millisecond),
|
|
},
|
|
{
|
|
Expr: "sum(up == 0 ) by (host)",
|
|
},
|
|
},
|
|
}, false, "either `record` or `alert` must be set")
|
|
|
|
// validate expressions
|
|
f(&Group{
|
|
Name: "test",
|
|
Rules: []Rule{
|
|
{
|
|
Record: "record",
|
|
Expr: "up | 0",
|
|
},
|
|
},
|
|
}, true, "invalid expression")
|
|
|
|
f(&Group{
|
|
Name: "test thanos",
|
|
Type: NewRawType("thanos"),
|
|
Rules: []Rule{
|
|
{Alert: "alert", Expr: "up == 1", Labels: map[string]string{
|
|
"description": "{{ value|query }}",
|
|
}},
|
|
},
|
|
}, true, "unknown datasource type")
|
|
|
|
f(&Group{
|
|
Name: "test graphite",
|
|
Type: NewGraphiteType(),
|
|
Rules: []Rule{
|
|
{Alert: "alert", Expr: "up == 1", Labels: map[string]string{
|
|
"description": "some-description",
|
|
}},
|
|
},
|
|
}, true, "bad graphite expr")
|
|
}
|
|
|
|
func TestGroupValidate_Success(t *testing.T) {
|
|
f := func(group *Group, validateAnnotations, validateExpressions bool) {
|
|
t.Helper()
|
|
|
|
var validateTplFn ValidateTplFn
|
|
if validateAnnotations {
|
|
validateTplFn = notifier.ValidateTemplates
|
|
}
|
|
err := group.Validate(validateTplFn, validateExpressions)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %s", err)
|
|
}
|
|
}
|
|
|
|
f(&Group{
|
|
Name: "test",
|
|
Rules: []Rule{
|
|
{
|
|
Record: "record",
|
|
Expr: "up | 0",
|
|
},
|
|
},
|
|
}, false, false)
|
|
|
|
f(&Group{
|
|
Name: "test",
|
|
Rules: []Rule{
|
|
{
|
|
Alert: "alert",
|
|
Expr: "up == 1",
|
|
Labels: map[string]string{
|
|
"summary": "{{ value|query }}",
|
|
},
|
|
},
|
|
},
|
|
}, false, false)
|
|
|
|
// validate annotiations
|
|
f(&Group{
|
|
Name: "test",
|
|
Rules: []Rule{
|
|
{
|
|
Alert: "alert",
|
|
Expr: "up == 1",
|
|
Labels: map[string]string{
|
|
"summary": `
|
|
{{ with printf "node_memory_MemTotal{job='node',instance='%s'}" "localhost" | query }}
|
|
{{ . | first | value | humanize1024 }}B
|
|
{{ end }}`,
|
|
},
|
|
},
|
|
},
|
|
}, true, false)
|
|
|
|
// validate expressions
|
|
f(&Group{
|
|
Name: "test prometheus",
|
|
Type: NewPrometheusType(),
|
|
Rules: []Rule{
|
|
{Alert: "alert", Expr: "up == 1", Labels: map[string]string{
|
|
"description": "{{ value|query }}",
|
|
}},
|
|
},
|
|
}, false, true)
|
|
}
|
|
|
|
func TestHashRule_NotEqual(t *testing.T) {
|
|
f := func(a, b Rule) {
|
|
t.Helper()
|
|
|
|
aID, bID := HashRule(a), HashRule(b)
|
|
if aID == bID {
|
|
t.Fatalf("rule hashes mustn't be equal; got %d", aID)
|
|
}
|
|
}
|
|
|
|
f(Rule{Alert: "record", Expr: "up == 1"}, Rule{Record: "record", Expr: "up == 1"})
|
|
|
|
f(Rule{Record: "record", Expr: "up == 1"}, Rule{Record: "record", Expr: "up == 2"})
|
|
|
|
f(Rule{Alert: "alert", Expr: "up == 1", Labels: map[string]string{
|
|
"foo": "bar",
|
|
"baz": "foo",
|
|
}}, Rule{Alert: "alert", Expr: "up == 1", Labels: map[string]string{
|
|
"baz": "foo",
|
|
"foo": "baz",
|
|
}})
|
|
|
|
f(Rule{Alert: "alert", Expr: "up == 1", Labels: map[string]string{
|
|
"foo": "bar",
|
|
"baz": "foo",
|
|
}}, Rule{Alert: "alert", Expr: "up == 1", Labels: map[string]string{
|
|
"baz": "foo",
|
|
}})
|
|
|
|
f(Rule{Alert: "alert", Expr: "up == 1", Labels: map[string]string{
|
|
"foo": "bar",
|
|
"baz": "foo",
|
|
}}, Rule{Alert: "alert", Expr: "up == 1"})
|
|
}
|
|
|
|
func TestHashRule_Equal(t *testing.T) {
|
|
f := func(a, b Rule) {
|
|
t.Helper()
|
|
|
|
aID, bID := HashRule(a), HashRule(b)
|
|
if aID != bID {
|
|
t.Fatalf("rule hashes must be equal; got %d and %d", aID, bID)
|
|
}
|
|
}
|
|
|
|
f(Rule{Record: "record", Expr: "up == 1"}, Rule{Record: "record", Expr: "up == 1"})
|
|
|
|
f(Rule{Alert: "alert", Expr: "up == 1"}, Rule{Alert: "alert", Expr: "up == 1"})
|
|
|
|
f(Rule{Alert: "alert", Expr: "up == 1", Labels: map[string]string{
|
|
"foo": "bar",
|
|
"baz": "foo",
|
|
}}, Rule{Alert: "alert", Expr: "up == 1", Labels: map[string]string{
|
|
"foo": "bar",
|
|
"baz": "foo",
|
|
}})
|
|
|
|
f(Rule{Alert: "alert", Expr: "up == 1", Labels: map[string]string{
|
|
"foo": "bar",
|
|
"baz": "foo",
|
|
}}, Rule{Alert: "alert", Expr: "up == 1", Labels: map[string]string{
|
|
"baz": "foo",
|
|
"foo": "bar",
|
|
}})
|
|
|
|
f(Rule{Alert: "record", Expr: "up == 1"}, Rule{Alert: "record", Expr: "up == 1"})
|
|
|
|
f(Rule{
|
|
Alert: "alert", Expr: "up == 1", For: promutils.NewDuration(time.Minute), KeepFiringFor: promutils.NewDuration(time.Minute),
|
|
}, Rule{Alert: "alert", Expr: "up == 1"})
|
|
}
|
|
|
|
func TestGroupChecksum(t *testing.T) {
|
|
f := func(t *testing.T, data, newData string) {
|
|
t.Helper()
|
|
var g Group
|
|
if err := yaml.Unmarshal([]byte(data), &g); err != nil {
|
|
t.Fatalf("failed to unmarshal: %s", err)
|
|
}
|
|
if g.Checksum == "" {
|
|
t.Fatalf("expected to get non-empty checksum")
|
|
}
|
|
|
|
var ng Group
|
|
if err := yaml.Unmarshal([]byte(newData), &ng); err != nil {
|
|
t.Fatalf("failed to unmarshal: %s", err)
|
|
}
|
|
if g.Checksum == ng.Checksum {
|
|
t.Fatalf("expected to get different checksums")
|
|
}
|
|
}
|
|
t.Run("Ok", func(t *testing.T) {
|
|
f(t, `
|
|
name: TestGroup
|
|
rules:
|
|
- alert: ExampleAlertAlwaysFiring
|
|
expr: sum by(job) (up == 1)
|
|
- record: handler:requests:rate5m
|
|
expr: sum(rate(prometheus_http_requests_total[5m])) by (handler)
|
|
`, `
|
|
name: TestGroup
|
|
rules:
|
|
- record: handler:requests:rate5m
|
|
expr: sum(rate(prometheus_http_requests_total[5m])) by (handler)
|
|
- alert: ExampleAlertAlwaysFiring
|
|
expr: sum by(job) (up == 1)
|
|
`)
|
|
})
|
|
|
|
t.Run("`for` change", func(t *testing.T) {
|
|
f(t, `
|
|
name: TestGroup
|
|
rules:
|
|
- alert: ExampleAlertWithFor
|
|
expr: sum by(job) (up == 1)
|
|
for: 5m
|
|
`, `
|
|
name: TestGroup
|
|
rules:
|
|
- alert: ExampleAlertWithFor
|
|
expr: sum by(job) (up == 1)
|
|
`)
|
|
})
|
|
t.Run("`interval` change", func(t *testing.T) {
|
|
f(t, `
|
|
name: TestGroup
|
|
interval: 2s
|
|
rules:
|
|
- alert: ExampleAlertWithFor
|
|
expr: sum by(job) (up == 1)
|
|
`, `
|
|
name: TestGroup
|
|
interval: 4s
|
|
rules:
|
|
- alert: ExampleAlertWithFor
|
|
expr: sum by(job) (up == 1)
|
|
`)
|
|
})
|
|
t.Run("`concurrency` change", func(t *testing.T) {
|
|
f(t, `
|
|
name: TestGroup
|
|
concurrency: 2
|
|
rules:
|
|
- alert: ExampleAlertWithFor
|
|
expr: sum by(job) (up == 1)
|
|
`, `
|
|
name: TestGroup
|
|
concurrency: 16
|
|
rules:
|
|
- alert: ExampleAlertWithFor
|
|
expr: sum by(job) (up == 1)
|
|
`)
|
|
})
|
|
|
|
t.Run("`params` change", func(t *testing.T) {
|
|
f(t, `
|
|
name: TestGroup
|
|
params:
|
|
nocache: ["1"]
|
|
rules:
|
|
- alert: foo
|
|
expr: sum by(job) (up == 1)
|
|
`, `
|
|
name: TestGroup
|
|
params:
|
|
nocache: ["0"]
|
|
rules:
|
|
- alert: foo
|
|
expr: sum by(job) (up == 1)
|
|
`)
|
|
})
|
|
|
|
t.Run("`limit` change", func(t *testing.T) {
|
|
f(t, `
|
|
name: TestGroup
|
|
limit: 5
|
|
rules:
|
|
- alert: foo
|
|
expr: sum by(job) (up == 1)
|
|
`, `
|
|
name: TestGroup
|
|
limit: 10
|
|
rules:
|
|
- alert: foo
|
|
expr: sum by(job) (up == 1)
|
|
`)
|
|
})
|
|
|
|
t.Run("`headers` change", func(t *testing.T) {
|
|
f(t, `
|
|
name: TestGroup
|
|
headers:
|
|
- "TenantID: foo"
|
|
rules:
|
|
- alert: foo
|
|
expr: sum by(job) (up == 1)
|
|
`, `
|
|
name: TestGroup
|
|
headers:
|
|
- "TenantID: bar"
|
|
rules:
|
|
- alert: foo
|
|
expr: sum by(job) (up == 1)
|
|
`)
|
|
})
|
|
|
|
t.Run("`notifier_headers` change", func(t *testing.T) {
|
|
f(t, `
|
|
name: TestGroup
|
|
notifier_headers:
|
|
- "TenantID: foo"
|
|
rules:
|
|
- alert: foo
|
|
expr: sum by(job) (up == 1)
|
|
`, `
|
|
name: TestGroup
|
|
notifier_headers:
|
|
- "TenantID: bar"
|
|
rules:
|
|
- alert: foo
|
|
expr: sum by(job) (up == 1)
|
|
`)
|
|
})
|
|
|
|
t.Run("`debug` change", func(t *testing.T) {
|
|
f(t, `
|
|
name: TestGroup
|
|
rules:
|
|
- alert: foo
|
|
expr: sum by(job) (up == 1)
|
|
`, `
|
|
name: TestGroup
|
|
rules:
|
|
- alert: foo
|
|
expr: sum by(job) (up == 1)
|
|
debug: true
|
|
`)
|
|
})
|
|
t.Run("`update_entries_limit` change", func(t *testing.T) {
|
|
f(t, `
|
|
name: TestGroup
|
|
rules:
|
|
- alert: foo
|
|
expr: sum by(job) (up == 1)
|
|
`, `
|
|
name: TestGroup
|
|
rules:
|
|
- alert: foo
|
|
expr: sum by(job) (up == 1)
|
|
update_entries_limit: 33
|
|
`)
|
|
})
|
|
}
|
|
|
|
func TestGroupParams(t *testing.T) {
|
|
f := func(t *testing.T, data string, expParams url.Values) {
|
|
t.Helper()
|
|
var g Group
|
|
if err := yaml.Unmarshal([]byte(data), &g); err != nil {
|
|
t.Fatalf("failed to unmarshal: %s", err)
|
|
}
|
|
got, exp := g.Params.Encode(), expParams.Encode()
|
|
if got != exp {
|
|
t.Fatalf("expected to have %q; got %q", exp, got)
|
|
}
|
|
}
|
|
|
|
t.Run("no params", func(t *testing.T) {
|
|
f(t, `
|
|
name: TestGroup
|
|
rules:
|
|
- alert: ExampleAlertAlwaysFiring
|
|
expr: sum by(job) (up == 1)
|
|
`, url.Values{})
|
|
})
|
|
|
|
t.Run("params", func(t *testing.T) {
|
|
f(t, `
|
|
name: TestGroup
|
|
params:
|
|
nocache: ["1"]
|
|
denyPartialResponse: ["true"]
|
|
rules:
|
|
- alert: ExampleAlertAlwaysFiring
|
|
expr: sum by(job) (up == 1)
|
|
`, url.Values{"nocache": {"1"}, "denyPartialResponse": {"true"}})
|
|
})
|
|
}
|