mirror of
https://github.com/prometheus/prometheus.git
synced 2025-03-05 20:59:13 -08:00
Merge pull request #1263 from prometheus/notify
Annotations for alerting rules
This commit is contained in:
commit
5af6dda58c
|
@ -59,9 +59,7 @@ type AlertStmt struct {
|
||||||
Expr Expr
|
Expr Expr
|
||||||
Duration time.Duration
|
Duration time.Duration
|
||||||
Labels model.LabelSet
|
Labels model.LabelSet
|
||||||
Summary string
|
Annotations model.LabelSet
|
||||||
Description string
|
|
||||||
Runbook string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// EvalStmt holds an expression and information on the range it should
|
// EvalStmt holds an expression and information on the range it should
|
||||||
|
|
|
@ -154,9 +154,7 @@ const (
|
||||||
itemIf
|
itemIf
|
||||||
itemFor
|
itemFor
|
||||||
itemWith
|
itemWith
|
||||||
itemSummary
|
itemAnnotations
|
||||||
itemRunbook
|
|
||||||
itemDescription
|
|
||||||
itemKeepCommon
|
itemKeepCommon
|
||||||
itemOffset
|
itemOffset
|
||||||
itemBy
|
itemBy
|
||||||
|
@ -186,9 +184,7 @@ var key = map[string]itemType{
|
||||||
"if": itemIf,
|
"if": itemIf,
|
||||||
"for": itemFor,
|
"for": itemFor,
|
||||||
"with": itemWith,
|
"with": itemWith,
|
||||||
"summary": itemSummary,
|
"annotations": itemAnnotations,
|
||||||
"runbook": itemRunbook,
|
|
||||||
"description": itemDescription,
|
|
||||||
"offset": itemOffset,
|
"offset": itemOffset,
|
||||||
"by": itemBy,
|
"by": itemBy,
|
||||||
"keeping_extra": itemKeepCommon,
|
"keeping_extra": itemKeepCommon,
|
||||||
|
|
|
@ -241,14 +241,8 @@ var tests = []struct {
|
||||||
input: "with",
|
input: "with",
|
||||||
expected: []item{{itemWith, 0, "with"}},
|
expected: []item{{itemWith, 0, "with"}},
|
||||||
}, {
|
}, {
|
||||||
input: "description",
|
input: "annotations",
|
||||||
expected: []item{{itemDescription, 0, "description"}},
|
expected: []item{{itemAnnotations, 0, "annotations"}},
|
||||||
}, {
|
|
||||||
input: "summary",
|
|
||||||
expected: []item{{itemSummary, 0, "summary"}},
|
|
||||||
}, {
|
|
||||||
input: "runbook",
|
|
||||||
expected: []item{{itemRunbook, 0, "runbook"}},
|
|
||||||
}, {
|
}, {
|
||||||
input: "offset",
|
input: "offset",
|
||||||
expected: []item{{itemOffset, 0, "offset"}},
|
expected: []item{{itemOffset, 0, "offset"}},
|
||||||
|
|
|
@ -357,9 +357,9 @@ func (p *parser) stmt() Statement {
|
||||||
|
|
||||||
// alertStmt parses an alert rule.
|
// alertStmt parses an alert rule.
|
||||||
//
|
//
|
||||||
// ALERT name IF expr [FOR duration] [WITH label_set]
|
// ALERT name IF expr [FOR duration]
|
||||||
// SUMMARY "summary"
|
// [WITH label_set]
|
||||||
// DESCRIPTION "description"
|
// [ANNOTATIONS label_set]
|
||||||
//
|
//
|
||||||
func (p *parser) alertStmt() *AlertStmt {
|
func (p *parser) alertStmt() *AlertStmt {
|
||||||
const ctx = "alert statement"
|
const ctx = "alert statement"
|
||||||
|
@ -389,44 +389,10 @@ func (p *parser) alertStmt() *AlertStmt {
|
||||||
lset = p.labelSet()
|
lset = p.labelSet()
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
annotations := model.LabelSet{}
|
||||||
hasSum, hasDesc, hasRunbook bool
|
if p.peek().typ == itemAnnotations {
|
||||||
sum, desc, runbook string
|
p.expect(itemAnnotations, ctx)
|
||||||
)
|
annotations = p.labelSet()
|
||||||
Loop:
|
|
||||||
for {
|
|
||||||
switch p.next().typ {
|
|
||||||
case itemSummary:
|
|
||||||
if hasSum {
|
|
||||||
p.errorf("summary must not be defined twice")
|
|
||||||
}
|
|
||||||
hasSum = true
|
|
||||||
sum = p.unquoteString(p.expect(itemString, ctx).val)
|
|
||||||
|
|
||||||
case itemDescription:
|
|
||||||
if hasDesc {
|
|
||||||
p.errorf("description must not be defined twice")
|
|
||||||
}
|
|
||||||
hasDesc = true
|
|
||||||
desc = p.unquoteString(p.expect(itemString, ctx).val)
|
|
||||||
|
|
||||||
case itemRunbook:
|
|
||||||
if hasRunbook {
|
|
||||||
p.errorf("runbook must not be defined twice")
|
|
||||||
}
|
|
||||||
hasRunbook = true
|
|
||||||
runbook = p.unquoteString(p.expect(itemString, ctx).val)
|
|
||||||
|
|
||||||
default:
|
|
||||||
p.backup()
|
|
||||||
break Loop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if sum == "" {
|
|
||||||
p.errorf("alert summary missing")
|
|
||||||
}
|
|
||||||
if desc == "" {
|
|
||||||
p.errorf("alert description missing")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return &AlertStmt{
|
return &AlertStmt{
|
||||||
|
@ -434,9 +400,7 @@ Loop:
|
||||||
Expr: expr,
|
Expr: expr,
|
||||||
Duration: duration,
|
Duration: duration,
|
||||||
Labels: lset,
|
Labels: lset,
|
||||||
Summary: sum,
|
Annotations: annotations,
|
||||||
Description: desc,
|
|
||||||
Runbook: runbook,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -874,11 +838,20 @@ func (p *parser) labelMatchers(operators ...itemType) metric.LabelMatchers {
|
||||||
|
|
||||||
matchers = append(matchers, m)
|
matchers = append(matchers, m)
|
||||||
|
|
||||||
|
if p.peek().typ == itemIdentifier {
|
||||||
|
p.errorf("missing comma before next identifier %q", p.peek().val)
|
||||||
|
}
|
||||||
|
|
||||||
// Terminate list if last matcher.
|
// Terminate list if last matcher.
|
||||||
if p.peek().typ != itemComma {
|
if p.peek().typ != itemComma {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
p.next()
|
p.next()
|
||||||
|
|
||||||
|
// Allow comma after each item in a multi-line listing.
|
||||||
|
if p.peek().typ == itemRightBrace {
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
p.expect(itemRightBrace, ctx)
|
p.expect(itemRightBrace, ctx)
|
||||||
|
|
|
@ -1153,15 +1153,19 @@ var testStatement = []struct {
|
||||||
service = "testservice"
|
service = "testservice"
|
||||||
# ... more fields here ...
|
# ... more fields here ...
|
||||||
}
|
}
|
||||||
SUMMARY "Global request rate low"
|
ANNOTATIONS {
|
||||||
DESCRIPTION "The global request rate is low"
|
summary = "Global request rate low",
|
||||||
|
description = "The global request rate is low"
|
||||||
|
}
|
||||||
|
|
||||||
foo = bar{label1="value1"}
|
foo = bar{label1="value1"}
|
||||||
|
|
||||||
ALERT BazAlert IF foo > 10
|
ALERT BazAlert IF foo > 10
|
||||||
DESCRIPTION "BazAlert"
|
ANNOTATIONS {
|
||||||
RUNBOOK "http://my.url"
|
description = "BazAlert",
|
||||||
SUMMARY "Baz"
|
runbook = "http://my.url",
|
||||||
|
summary = "Baz",
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
expected: Statements{
|
expected: Statements{
|
||||||
&RecordStmt{
|
&RecordStmt{
|
||||||
|
@ -1198,8 +1202,10 @@ var testStatement = []struct {
|
||||||
}},
|
}},
|
||||||
Labels: model.LabelSet{"service": "testservice"},
|
Labels: model.LabelSet{"service": "testservice"},
|
||||||
Duration: 5 * time.Minute,
|
Duration: 5 * time.Minute,
|
||||||
Summary: "Global request rate low",
|
Annotations: model.LabelSet{
|
||||||
Description: "The global request rate is low",
|
"summary": "Global request rate low",
|
||||||
|
"description": "The global request rate is low",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
&RecordStmt{
|
&RecordStmt{
|
||||||
Name: "foo",
|
Name: "foo",
|
||||||
|
@ -1225,9 +1231,11 @@ var testStatement = []struct {
|
||||||
RHS: &NumberLiteral{10},
|
RHS: &NumberLiteral{10},
|
||||||
},
|
},
|
||||||
Labels: model.LabelSet{},
|
Labels: model.LabelSet{},
|
||||||
Summary: "Baz",
|
Annotations: model.LabelSet{
|
||||||
Description: "BazAlert",
|
"summary": "Baz",
|
||||||
Runbook: "http://my.url",
|
"description": "BazAlert",
|
||||||
|
"runbook": "http://my.url",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
|
@ -1248,8 +1256,10 @@ var testStatement = []struct {
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
input: `ALERT SomeName IF some_metric > 1
|
input: `ALERT SomeName IF some_metric > 1
|
||||||
SUMMARY "Global request rate low"
|
ANNOTATIONS {
|
||||||
DESCRIPTION "The global request rate is low"
|
summary = "Global request rate low",
|
||||||
|
description = "The global request rate is low"
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
expected: Statements{
|
expected: Statements{
|
||||||
&AlertStmt{
|
&AlertStmt{
|
||||||
|
@ -1265,8 +1275,10 @@ var testStatement = []struct {
|
||||||
RHS: &NumberLiteral{1},
|
RHS: &NumberLiteral{1},
|
||||||
},
|
},
|
||||||
Labels: model.LabelSet{},
|
Labels: model.LabelSet{},
|
||||||
Summary: "Global request rate low",
|
Annotations: model.LabelSet{
|
||||||
Description: "The global request rate is low",
|
"summary": "Global request rate low",
|
||||||
|
"description": "The global request rate is low",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
|
@ -1276,8 +1288,10 @@ var testStatement = []struct {
|
||||||
service = "testservice"
|
service = "testservice"
|
||||||
# ... more fields here ...
|
# ... more fields here ...
|
||||||
}
|
}
|
||||||
SUMMARY "Global request rate low"
|
ANNOTATIONS {
|
||||||
DESCRIPTION "The global request rate is low"
|
summary = "Global request rate low"
|
||||||
|
description = "The global request rate is low"
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
fail: true,
|
fail: true,
|
||||||
}, {
|
}, {
|
||||||
|
@ -1323,16 +1337,6 @@ var testStatement = []struct {
|
||||||
DESCRIPTION "The global request rate is low"
|
DESCRIPTION "The global request rate is low"
|
||||||
`,
|
`,
|
||||||
fail: true,
|
fail: true,
|
||||||
}, {
|
|
||||||
input: `ALERT SomeName IF some_metric > 1 WITH {}
|
|
||||||
SUMMARY "Global request rate low"
|
|
||||||
`,
|
|
||||||
fail: true,
|
|
||||||
}, {
|
|
||||||
input: `ALERT SomeName IF some_metric > 1
|
|
||||||
DESCRIPTION "The global request rate is low"
|
|
||||||
`,
|
|
||||||
fail: true,
|
|
||||||
},
|
},
|
||||||
// Fuzzing regression tests.
|
// Fuzzing regression tests.
|
||||||
{
|
{
|
||||||
|
|
|
@ -109,8 +109,9 @@ func (node *AlertStmt) String() string {
|
||||||
if len(node.Labels) > 0 {
|
if len(node.Labels) > 0 {
|
||||||
s += fmt.Sprintf("\n\tWITH %s", node.Labels)
|
s += fmt.Sprintf("\n\tWITH %s", node.Labels)
|
||||||
}
|
}
|
||||||
s += fmt.Sprintf("\n\tSUMMARY %q", node.Summary)
|
if len(node.Annotations) > 0 {
|
||||||
s += fmt.Sprintf("\n\tDESCRIPTION %q", node.Description)
|
s += fmt.Sprintf("\n\tANNOTATIONS %s", node.Labels)
|
||||||
|
}
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -105,12 +105,8 @@ type AlertingRule struct {
|
||||||
holdDuration time.Duration
|
holdDuration time.Duration
|
||||||
// Extra labels to attach to the resulting alert sample vectors.
|
// Extra labels to attach to the resulting alert sample vectors.
|
||||||
labels model.LabelSet
|
labels model.LabelSet
|
||||||
// Short alert summary, suitable for email subjects.
|
// Non-identifying key/value pairs.
|
||||||
summary string
|
annotations model.LabelSet
|
||||||
// More detailed alert description.
|
|
||||||
description string
|
|
||||||
// A reference to a runbook for the alert.
|
|
||||||
runbook string
|
|
||||||
|
|
||||||
// Protects the below.
|
// Protects the below.
|
||||||
mutex sync.Mutex
|
mutex sync.Mutex
|
||||||
|
@ -120,23 +116,13 @@ type AlertingRule struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAlertingRule constructs a new AlertingRule.
|
// NewAlertingRule constructs a new AlertingRule.
|
||||||
func NewAlertingRule(
|
func NewAlertingRule(name string, vec promql.Expr, hold time.Duration, lbls, anns model.LabelSet) *AlertingRule {
|
||||||
name string,
|
|
||||||
vector promql.Expr,
|
|
||||||
holdDuration time.Duration,
|
|
||||||
labels model.LabelSet,
|
|
||||||
summary string,
|
|
||||||
description string,
|
|
||||||
runbook string,
|
|
||||||
) *AlertingRule {
|
|
||||||
return &AlertingRule{
|
return &AlertingRule{
|
||||||
name: name,
|
name: name,
|
||||||
vector: vector,
|
vector: vec,
|
||||||
holdDuration: holdDuration,
|
holdDuration: hold,
|
||||||
labels: labels,
|
labels: lbls,
|
||||||
summary: summary,
|
annotations: anns,
|
||||||
description: description,
|
|
||||||
runbook: runbook,
|
|
||||||
|
|
||||||
activeAlerts: map[model.Fingerprint]*Alert{},
|
activeAlerts: map[model.Fingerprint]*Alert{},
|
||||||
}
|
}
|
||||||
|
@ -217,9 +203,9 @@ func (rule *AlertingRule) String() string {
|
||||||
if len(rule.labels) > 0 {
|
if len(rule.labels) > 0 {
|
||||||
s += fmt.Sprintf("\n\tWITH %s", rule.labels)
|
s += fmt.Sprintf("\n\tWITH %s", rule.labels)
|
||||||
}
|
}
|
||||||
s += fmt.Sprintf("\n\tSUMMARY %q", rule.summary)
|
if len(rule.annotations) > 0 {
|
||||||
s += fmt.Sprintf("\n\tDESCRIPTION %q", rule.description)
|
s += fmt.Sprintf("\n\tANNOTATIONS %s", rule.annotations)
|
||||||
s += fmt.Sprintf("\n\tRUNBOOK %q", rule.runbook)
|
}
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -239,9 +225,9 @@ func (rule *AlertingRule) HTMLSnippet(pathPrefix string) template.HTML {
|
||||||
if len(rule.labels) > 0 {
|
if len(rule.labels) > 0 {
|
||||||
s += fmt.Sprintf("\n WITH %s", rule.labels)
|
s += fmt.Sprintf("\n WITH %s", rule.labels)
|
||||||
}
|
}
|
||||||
s += fmt.Sprintf("\n SUMMARY %q", rule.summary)
|
if len(rule.annotations) > 0 {
|
||||||
s += fmt.Sprintf("\n DESCRIPTION %q", rule.description)
|
s += fmt.Sprintf("\n ANNOTATIONS %s", rule.annotations)
|
||||||
s += fmt.Sprintf("\n RUNBOOK %q", rule.runbook)
|
}
|
||||||
return template.HTML(s)
|
return template.HTML(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -208,23 +208,22 @@ func (m *Manager) sendAlertNotifications(rule *AlertingRule, timestamp model.Tim
|
||||||
// who are not used to Go's templating system.
|
// who are not used to Go's templating system.
|
||||||
defs := "{{$labels := .Labels}}{{$value := .Value}}"
|
defs := "{{$labels := .Labels}}{{$value := .Value}}"
|
||||||
|
|
||||||
expand := func(text string) string {
|
expand := func(text model.LabelValue) model.LabelValue {
|
||||||
tmpl := template.NewTemplateExpander(defs+text, "__alert_"+rule.Name(), tmplData, timestamp, m.queryEngine, m.externalURL.Path)
|
tmpl := template.NewTemplateExpander(defs+string(text), "__alert_"+rule.Name(), tmplData, timestamp, m.queryEngine, m.externalURL.Path)
|
||||||
result, err := tmpl.Expand()
|
result, err := tmpl.Expand()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result = err.Error()
|
result = err.Error()
|
||||||
log.Warnf("Error expanding alert template %v with data '%v': %v", rule.Name(), tmplData, err)
|
log.Warnf("Error expanding alert template %v with data '%v': %v", rule.Name(), tmplData, err)
|
||||||
}
|
}
|
||||||
return result
|
return model.LabelValue(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
labels := aa.Labels.Clone()
|
labels := aa.Labels.Clone()
|
||||||
labels[model.AlertNameLabel] = model.LabelValue(rule.Name())
|
labels[model.AlertNameLabel] = model.LabelValue(rule.Name())
|
||||||
|
|
||||||
annotations := model.LabelSet{
|
annotations := rule.annotations.Clone()
|
||||||
"summary": model.LabelValue(expand(rule.summary)),
|
for an, av := range rule.annotations {
|
||||||
"description": model.LabelValue(expand(rule.description)),
|
annotations[an] = expand(av)
|
||||||
"runbook": model.LabelValue(expand(rule.runbook)),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
alerts = append(alerts, &model.Alert{
|
alerts = append(alerts, &model.Alert{
|
||||||
|
@ -359,7 +358,7 @@ func (m *Manager) loadRuleFiles(filenames ...string) error {
|
||||||
for _, stmt := range stmts {
|
for _, stmt := range stmts {
|
||||||
switch r := stmt.(type) {
|
switch r := stmt.(type) {
|
||||||
case *promql.AlertStmt:
|
case *promql.AlertStmt:
|
||||||
rule := NewAlertingRule(r.Name, r.Expr, r.Duration, r.Labels, r.Summary, r.Description, r.Runbook)
|
rule := NewAlertingRule(r.Name, r.Expr, r.Duration, r.Labels, r.Annotations)
|
||||||
m.rules = append(m.rules, rule)
|
m.rules = append(m.rules, rule)
|
||||||
case *promql.RecordStmt:
|
case *promql.RecordStmt:
|
||||||
rule := NewRecordingRule(r.Name, r.Expr, r.Labels)
|
rule := NewRecordingRule(r.Name, r.Expr, r.Labels)
|
||||||
|
|
|
@ -56,7 +56,7 @@ func TestAlertingRule(t *testing.T) {
|
||||||
expr,
|
expr,
|
||||||
time.Minute,
|
time.Minute,
|
||||||
model.LabelSet{"severity": "critical"},
|
model.LabelSet{"severity": "critical"},
|
||||||
"summary", "description", "runbook",
|
model.LabelSet{},
|
||||||
)
|
)
|
||||||
|
|
||||||
var tests = []struct {
|
var tests = []struct {
|
||||||
|
|
Loading…
Reference in a new issue