Skip to content

Commit 8a251ed

Browse files
committed
feat(alerta-service): Add custom severities to Alerta handler
Fix #2056 This commit allows usage of all or some Alerta Severities. It provides two keywords to fine tune kapacitor built-in severities. 1. First, you can rename kapacitor serverity levels: crit, warn, info, ok to any other severities configured in your Alerta: |alert() // ... .alerta() // ... .renameSeverity('crit', 'major') .renameSeverity('info', 'notice') I suppose this will cover most of the cases. But if you do want a lot of severity levels: 2. You can add custom severity levels, which will be avaluated on Alerta handler level after built-in alert was triggered. |alert() // ... .warn(lambda: "cpu" > 50) .alerta() // ... .addSeverity('minor', 3, lambda: "cpu" > 60) .addSeverity('major', 2, lambda: "cpu" > 70) .addSeverity('critical', 1, lambda: "cpu" > 80) .addSeverity('fatal', 0, lambda: "cpu" > 90) Note: evaluation of addSeverity condition only happen after build-in alert is triggered, so you need some entry point (like .warn() in exmple), which should cover all range of values interesting to you. Note: this severities use Alerta's code order - higher severity has lower code (0 for fatal, 9 for ok) Note: .addSeverity() is quite useless in combination with .stateChangesOnly(), but Alerta has decent deduplication mechanism, so it shouldn't be a problem
1 parent cee3f4e commit 8a251ed

9 files changed

Lines changed: 207 additions & 19 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
- [#2559](https://github.com/influxdata/kapacitor/pull/2559): kapacitor cli supports flux tasks
1313
- [#2560](https://github.com/influxdata/kapacitor/pull/2560): enable new-style slack apps
1414
- [#2576](https://github.com/influxdata/kapacitor/pull/2576): shared secret auth to influxdb in OSS
15+
- [#2584](https://github.com/influxdata/kapacitor/pull/2584): Add custom severities to Alerta handler
1516

1617
### Bugfixes
1718
- [#2564](https://github.com/influxdata/kapacitor/pull/2564): Fix a panic in the scraper handler when debug mode is enabled

alert.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,23 @@ func newAlertNode(et *ExecutingTask, n *pipeline.AlertNode, d NodeDiagnostic) (a
366366
if a.Environment != "" {
367367
c.Environment = a.Environment
368368
}
369+
if len(a.RenameSeverities) != 0 {
370+
c.RenameSeverities = a.RenameSeverities
371+
}
372+
if len(a.ExtraSeverities) != 0 {
373+
c.ExtraSeverityExpressions = make([]stateful.Expression, len(a.ExtraSeverities))
374+
c.ExtraSeverityNames = make([]string, len(a.ExtraSeverities))
375+
c.ExtraSeverityScopePools = make([]stateful.ScopePool, len(a.ExtraSeverities))
376+
for i, severity := range a.ExtraSeverities {
377+
statefulExpression, expressionCompileError := stateful.NewExpression(severity.Condition.Expression)
378+
if expressionCompileError != nil {
379+
return nil, fmt.Errorf("Failed to compile stateful expression for Alerta extra severity %s: %s", severity.Name, expressionCompileError)
380+
}
381+
c.ExtraSeverityExpressions[i] = statefulExpression
382+
c.ExtraSeverityNames[i] = severity.Name
383+
c.ExtraSeverityScopePools[i] = stateful.NewScopePool(ast.FindReferenceVariables(severity.Condition.Expression))
384+
}
385+
}
369386
if a.Group != "" {
370387
c.Group = a.Group
371388
}

integrations/streamer_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9197,11 +9197,17 @@ stream
91979197
.token('testtoken1234567')
91989198
.environment('production')
91999199
.timeout(1h)
9200+
.alerta()
9201+
.token('testtoken7654321')
9202+
.environment('production')
9203+
.timeout(1h)
9204+
.addSeverity('fatal', 0, lambda: "count" > 9.0)
92009205
.alerta()
92019206
.token('anothertesttoken')
92029207
.resource('resource: {{ index .Tags "host" }}')
92039208
.event('event: {{ .TaskName }}')
92049209
.environment('{{ index .Tags "host" }}')
9210+
.renameSeverity('crit', 'major')
92059211
.origin('override')
92069212
.group('{{ .ID }}')
92079213
.value('{{ index .Fields "count" }}')
@@ -9227,6 +9233,23 @@ stream
92279233
Event: "serverA",
92289234
Group: "host=serverA",
92299235
Environment: "production",
9236+
Severity: "critical",
9237+
Text: "kapacitor/cpu/serverA is CRITICAL @1971-01-01 00:00:10 +0000 UTC",
9238+
Origin: "Kapacitor",
9239+
Service: []string{"cpu"},
9240+
Correlate: []string{"cpu"},
9241+
Timeout: 3600,
9242+
},
9243+
},
9244+
alertatest.Request{
9245+
URL: "/alert",
9246+
Authorization: "Bearer testtoken7654321",
9247+
PostData: alertatest.PostData{
9248+
Resource: "cpu",
9249+
Event: "serverA",
9250+
Group: "host=serverA",
9251+
Environment: "production",
9252+
Severity: "fatal",
92309253
Text: "kapacitor/cpu/serverA is CRITICAL @1971-01-01 00:00:10 +0000 UTC",
92319254
Origin: "Kapacitor",
92329255
Service: []string{"cpu"},
@@ -9242,6 +9265,7 @@ stream
92429265
Event: "event: TestStream_Alert",
92439266
Group: "serverA",
92449267
Environment: "serverA",
9268+
Severity: "major",
92459269
Text: "kapacitor/cpu/serverA is CRITICAL @1971-01-01 00:00:10 +0000 UTC",
92469270
Origin: "override",
92479271
Service: []string{"serviceA", "serviceB", "cpu"},

pipeline/alert.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"encoding/json"
55
"fmt"
66
"reflect"
7+
"sort"
78
"strings"
89
"time"
910

@@ -1219,6 +1220,12 @@ func (n *AlertNodeData) Alerta() *AlertaHandler {
12191220
return alerta
12201221
}
12211222

1223+
type AlertaCustomSeverity struct {
1224+
Name string `json:"name"`
1225+
Code int64 `json:"code"`
1226+
Condition *ast.LambdaNode `json:"condition"`
1227+
}
1228+
12221229
// tick:embedded:AlertNode.Alerta
12231230
type AlertaHandler struct {
12241231
*AlertNodeData `json:"-"`
@@ -1242,6 +1249,13 @@ type AlertaHandler struct {
12421249
// Defaut is set from the configuration.
12431250
Environment string `json:"environment"`
12441251

1252+
// Alerta supports many different severity levels. And it allows you to add even more.
1253+
// To benefit from this model we can use two ways:
1254+
// Rename kapacitor built-in severities to match some of alerta's severities
1255+
RenameSeverities map[string]string `tick:"RenameSeverity" json:"rename-severities"`
1256+
// Add post-processing to kapacitor alerts to fine tune severity levels
1257+
ExtraSeverities []*AlertaCustomSeverity `tick:"AddSeverity" json:"add-severities"`
1258+
12451259
// Alerta group.
12461260
// Can be a template and has access to the same data as the AlertNode.Details property.
12471261
// Default: {{ .Group }}
@@ -1268,6 +1282,24 @@ type AlertaHandler struct {
12681282
Timeout time.Duration `json:"timeout"`
12691283
}
12701284

1285+
func (a *AlertaHandler) RenameSeverity(kapacitorName string, alertaName string) *AlertaHandler {
1286+
if a.RenameSeverities == nil {
1287+
a.RenameSeverities = make(map[string]string)
1288+
}
1289+
a.RenameSeverities[kapacitorName] = alertaName
1290+
return a
1291+
}
1292+
func (a *AlertaHandler) AddSeverity(name string, code int64, condition *ast.LambdaNode) *AlertaHandler {
1293+
a.ExtraSeverities = append(a.ExtraSeverities, &AlertaCustomSeverity{
1294+
Name: name,
1295+
Code: code,
1296+
Condition: condition,
1297+
})
1298+
// Alerta severities have ascending order: higher severity has lower code (1 for critical, 9 for ok)
1299+
sort.SliceStable(a.ExtraSeverities, func(i, j int) bool { return a.ExtraSeverities[i].Code > a.ExtraSeverities[j].Code })
1300+
return a
1301+
}
1302+
12711303
// List of effected services.
12721304
// If not specified defaults to the Name of the stream.
12731305
// tick:property

pipeline/tick/alert.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -220,8 +220,20 @@ func (n *AlertNode) Build(a *pipeline.AlertNode) (ast.Node, error) {
220220
Dot("token", h.Token).
221221
Dot("resource", h.Resource).
222222
Dot("event", h.Event).
223-
Dot("environment", h.Environment).
224-
Dot("group", h.Group).
223+
Dot("environment", h.Environment)
224+
225+
var severitiesOrder = []string{"ok", "info", "warn", "crit"}
226+
for _, k := range severitiesOrder {
227+
if val, ok := h.RenameSeverities[k]; ok {
228+
n.Dot("renameSeverity", k, val)
229+
}
230+
}
231+
232+
for _, k := range h.ExtraSeverities {
233+
n.Dot("addSeverity", k.Name, k.Code, k.Condition)
234+
}
235+
236+
n.Dot("group", h.Group).
225237
Dot("value", h.Value).
226238
Dot("origin", h.Origin).
227239
Dot("services", args(h.Service)...).

pipeline/tick/alert_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -661,6 +661,14 @@ func TestAlertAlerta(t *testing.T) {
661661
handler.Resource = "Harbinger"
662662
handler.Event = "Jump through Omega-4 Relay"
663663
handler.Environment = "Collector base"
664+
handler.RenameSeverities = make(map[string]string)
665+
handler.RenameSeverities["info"] = "notice"
666+
handler.ExtraSeverities = make([]*pipeline.AlertaCustomSeverity, 1)
667+
handler.ExtraSeverities[0] = &pipeline.AlertaCustomSeverity{
668+
Name: "major",
669+
Code: 2,
670+
Condition: newLambda(85),
671+
}
664672
handler.Group = "I brought Jack, Miranda and Tali"
665673
handler.Value = "Save the Galaxy"
666674
handler.Origin = "Omega"
@@ -680,6 +688,8 @@ func TestAlertAlerta(t *testing.T) {
680688
.resource('Harbinger')
681689
.event('Jump through Omega-4 Relay')
682690
.environment('Collector base')
691+
.renameSeverity('info', 'notice')
692+
.addSeverity('major', 2, lambda: "cpu" > 85)
683693
.group('I brought Jack, Miranda and Tali')
684694
.value('Save the Galaxy')
685695
.origin('Omega')

server/server_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10149,6 +10149,7 @@ func TestServer_AlertHandlers(t *testing.T) {
1014910149
Event: "id",
1015010150
Group: "test",
1015110151
Environment: "env",
10152+
Severity: "critical",
1015210153
Text: "message",
1015310154
Origin: "kapacitor",
1015410155
Service: []string{"alert"},

services/alerta/alertatest/alertatest.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ type PostData struct {
5757
Event string `json:"event"`
5858
Group string `json:"group"`
5959
Environment string `json:"environment"`
60+
Severity string `json:"severity"`
6061
Text string `json:"text"`
6162
Origin string `json:"origin"`
6263
Service []string `json:"service"`

services/alerta/service.go

Lines changed: 107 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import (
1717
khttp "github.com/influxdata/kapacitor/http"
1818
"github.com/influxdata/kapacitor/keyvalue"
1919
"github.com/influxdata/kapacitor/models"
20+
"github.com/influxdata/kapacitor/tick/ast"
21+
"github.com/influxdata/kapacitor/tick/stateful"
2022
"github.com/pkg/errors"
2123
)
2224

@@ -267,6 +269,16 @@ type HandlerConfig struct {
267269
// Defaut is set from the configuration.
268270
Environment string `mapstructure:"environment"`
269271

272+
// Renaming rules for severities
273+
// Allows to rewrite build-in kapacitor severities (info, warn, crit) to any of Alerta multiple severities
274+
RenameSeverities map[string]string `mapstructure:"rename-severities"`
275+
276+
// Expressions for custom Alerta severities
277+
// Allows to fine tune severity levels of kapacitor
278+
ExtraSeverityExpressions []stateful.Expression `mapstructure:"severity-expressions"`
279+
ExtraSeverityNames []string `mapstructure:"severity-names"`
280+
ExtraSeverityScopePools []stateful.ScopePool `mapstructure:"severity-scope-pool"`
281+
270282
// Alerta group.
271283
// Can be a template and has access to the same data as the AlertNode.Details property.
272284
// Default: {{ .Group }}
@@ -297,13 +309,17 @@ type handler struct {
297309
c HandlerConfig
298310
diag Diagnostic
299311

300-
resourceTmpl *text.Template
301-
eventTmpl *text.Template
302-
environmentTmpl *text.Template
303-
valueTmpl *text.Template
304-
groupTmpl *text.Template
305-
serviceTmpl []*text.Template
306-
correlateTmpl []*text.Template
312+
resourceTmpl *text.Template
313+
eventTmpl *text.Template
314+
environmentTmpl *text.Template
315+
renameSeverities map[string]string
316+
severityLevels []string
317+
severityExpressions []stateful.Expression
318+
scopePools []stateful.ScopePool
319+
valueTmpl *text.Template
320+
groupTmpl *text.Template
321+
serviceTmpl []*text.Template
322+
correlateTmpl []*text.Template
307323
}
308324

309325
func (s *Service) DefaultHandlerConfig() HandlerConfig {
@@ -357,16 +373,20 @@ func (s *Service) Handler(c HandlerConfig, ctx ...keyvalue.T) (alert.Handler, er
357373
}
358374

359375
return &handler{
360-
s: s,
361-
c: c,
362-
diag: s.diag.WithContext(ctx...),
363-
resourceTmpl: rtmpl,
364-
eventTmpl: evtmpl,
365-
environmentTmpl: etmpl,
366-
groupTmpl: gtmpl,
367-
valueTmpl: vtmpl,
368-
serviceTmpl: stmpl,
369-
correlateTmpl: ctmpl,
376+
s: s,
377+
c: c,
378+
diag: s.diag.WithContext(ctx...),
379+
resourceTmpl: rtmpl,
380+
eventTmpl: evtmpl,
381+
environmentTmpl: etmpl,
382+
renameSeverities: c.RenameSeverities,
383+
severityLevels: c.ExtraSeverityNames,
384+
severityExpressions: c.ExtraSeverityExpressions,
385+
scopePools: c.ExtraSeverityScopePools,
386+
groupTmpl: gtmpl,
387+
valueTmpl: vtmpl,
388+
serviceTmpl: stmpl,
389+
correlateTmpl: ctmpl,
370390
}, nil
371391
}
372392

@@ -468,20 +488,39 @@ func (h *handler) Handle(event alert.Event) {
468488
}
469489

470490
var severity string
491+
var severityKey string
471492

472493
switch event.State.Level {
473494
case alert.OK:
474495
severity = "ok"
496+
severityKey = "ok"
475497
case alert.Info:
476498
severity = "informational"
499+
severityKey = "info"
477500
case alert.Warning:
478501
severity = "warning"
502+
severityKey = "warn"
479503
case alert.Critical:
480504
severity = "critical"
505+
severityKey = "crit"
481506
default:
482507
severity = "indeterminate"
483508
}
484509

510+
if val, ok := h.renameSeverities[severityKey]; ok {
511+
severity = val
512+
}
513+
514+
if len(h.severityLevels) != 0 {
515+
for i, expression := range h.severityExpressions {
516+
if pass, err := EvalPredicate(expression, h.scopePools[i], event.State.Time, event.Data.Fields, event.Data.Tags); err != nil {
517+
h.diag.Error("error evaluating expression for Alerta severity", err)
518+
} else if pass {
519+
severity = h.severityLevels[i]
520+
}
521+
}
522+
}
523+
485524
if err := h.s.Alert(
486525
h.c.Token,
487526
h.c.TokenPrefix,
@@ -502,3 +541,54 @@ func (h *handler) Handle(event alert.Event) {
502541
h.diag.Error("failed to send event to Alerta", err)
503542
}
504543
}
544+
545+
func EvalPredicate(se stateful.Expression, scopePool stateful.ScopePool, now time.Time, fields models.Fields, tags models.Tags) (bool, error) {
546+
vars := scopePool.Get()
547+
defer scopePool.Put(vars)
548+
err := fillScope(vars, scopePool.ReferenceVariables(), now, fields, tags)
549+
if err != nil {
550+
return false, err
551+
}
552+
553+
// for function signature check
554+
if _, err := se.Type(vars); err != nil {
555+
return false, err
556+
}
557+
558+
return se.EvalBool(vars)
559+
}
560+
561+
// fillScope - given a scope and reference variables, we fill the exact variables from the now, fields and tags.
562+
func fillScope(vars *stateful.Scope, referenceVariables []string, now time.Time, fields models.Fields, tags models.Tags) error {
563+
for _, refVariableName := range referenceVariables {
564+
if refVariableName == "time" {
565+
vars.Set("time", now.Local())
566+
continue
567+
}
568+
569+
// Support the error with tags/fields collision
570+
var fieldValue interface{}
571+
var isFieldExists bool
572+
var tagValue interface{}
573+
var isTagExists bool
574+
575+
if fieldValue, isFieldExists = fields[refVariableName]; isFieldExists {
576+
vars.Set(refVariableName, fieldValue)
577+
}
578+
579+
if tagValue, isTagExists = tags[refVariableName]; isTagExists {
580+
if isFieldExists {
581+
return fmt.Errorf("cannot have field and tags with same name %q", refVariableName)
582+
}
583+
vars.Set(refVariableName, tagValue)
584+
}
585+
if !isFieldExists && !isTagExists {
586+
if !vars.Has(refVariableName) {
587+
vars.Set(refVariableName, ast.MissingValue)
588+
}
589+
590+
}
591+
}
592+
593+
return nil
594+
}

0 commit comments

Comments
 (0)