Skip to content

Commit

Permalink
feat(alerts): templating for address in email alert (#2650)
Browse files Browse the repository at this point in the history
* feat(alerts): templating for address in email alert

* chore: go fmt

* chore: go mod tidy
  • Loading branch information
docmerlin authored Dec 9, 2021
1 parent af204c7 commit 4c2b965
Show file tree
Hide file tree
Showing 9 changed files with 288 additions and 94 deletions.
3 changes: 2 additions & 1 deletion alert.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,8 @@ func newAlertNode(et *ExecutingTask, n *pipeline.AlertNode, d NodeDiagnostic) (a

for _, email := range n.EmailHandlers {
c := smtp.HandlerConfig{
To: email.ToList,
To: email.ToList,
ToTemplates: email.ToTemplatesList,
}
h := et.tm.SMTPService.Handler(c, ctx...)
an.handlers = append(an.handlers, h)
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ require (
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad
golang.org/x/tools v0.1.0
google.golang.org/protobuf v1.27.1
gopkg.in/gomail.v2 v2.0.0-20150902115704-41f357289737
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
)

require (
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1705,8 +1705,8 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify/fsnotify.v1 v1.4.7 h1:XNNYLJHt73EyYiCZi6+xjupS9CpvmiDgjPTAjrBlQbo=
gopkg.in/fsnotify/fsnotify.v1 v1.4.7/go.mod h1:Fyux9zXlo4rWoMSIzpn9fDAYjalPqJ/K1qJ27s+7ltE=
gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
gopkg.in/gomail.v2 v2.0.0-20150902115704-41f357289737 h1:NvePS/smRcFQ4bMtTddFtknbGCtoBkJxGmpSpVRafCc=
gopkg.in/gomail.v2 v2.0.0-20150902115704-41f357289737/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
Expand Down
274 changes: 199 additions & 75 deletions integrations/streamer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10775,108 +10775,231 @@ stream
}

func TestStream_AlertEmail(t *testing.T) {
var script = `
stream
|from()
.measurement('cpu')
.where(lambda: "host" == 'serverA')
.groupBy('host')
|window()
.period(10s)
.every(10s)
|count('value')
|alert()
.id('kapacitor.{{ .Name }}.{{ index .Tags "host" }}')
.details('''
var cases = []struct {
name string
script string
expected []*smtptest.Message
}{
{
name: "emails directly",
script: `stream
|from()
.measurement('cpu')
.where(lambda: "host" == 'serverA')
.groupBy('host')
|window()
.period(10s)
.every(10s)
|count('value')
|alert()
.id('kapacitor.{{ .Name }}.{{ index .Tags "host" }}')
.details('''
<b>{{.Message}}</b>
Value: {{ index .Fields "count" }}
<a href="http://graphs.example.com/host/{{index .Tags "host"}}">Details</a>
''')
.info(lambda: "count" > 6.0)
.warn(lambda: "count" > 7.0)
.crit(lambda: "count" > 8.0)
.email('[email protected]', '[email protected]')
.email()
.to('[email protected]', '[email protected]')
`
.info(lambda: "count" > 6.0)
.warn(lambda: "count" > 7.0)
.crit(lambda: "count" > 8.0)
.email('[email protected]', '[email protected]')
.email()
.to('[email protected]', '[email protected]')
`,
expected: []*smtptest.Message{
{
Header: mail.Header{
"Mime-Version": []string{"1.0"},
"Content-Type": []string{"text/html; charset=UTF-8"},
"Content-Transfer-Encoding": []string{"quoted-printable"},
"To": []string{"[email protected], [email protected]"},
"From": []string{"[email protected]"},
"Subject": []string{"kapacitor.cpu.serverA is CRITICAL"},
},
Body: `
<b>kapacitor.cpu.serverA is CRITICAL</b>
Value: 10
<a href=3D"http://graphs.example.com/host/serverA">Details</a>
`,
},
{
Header: mail.Header{
"Mime-Version": []string{"1.0"},
"Content-Type": []string{"text/html; charset=UTF-8"},
"Content-Transfer-Encoding": []string{"quoted-printable"},
"To": []string{"[email protected], [email protected]"},
"From": []string{"[email protected]"},
"Subject": []string{"kapacitor.cpu.serverA is CRITICAL"},
},
Body: `
<b>kapacitor.cpu.serverA is CRITICAL</b>
expMail := []*smtptest.Message{
Value: 10
<a href=3D"http://graphs.example.com/host/serverA">Details</a>
`,
}},
},
{
Header: mail.Header{
"Mime-Version": []string{"1.0"},
"Content-Type": []string{"text/html; charset=UTF-8"},
"Content-Transfer-Encoding": []string{"quoted-printable"},
"To": []string{"[email protected], [email protected]"},
"From": []string{"[email protected]"},
"Subject": []string{"kapacitor.cpu.serverA is CRITICAL"},
},
Body: `
name: "emails directly and in fields",
script: `stream
|from()
.measurement('cpu')
.where(lambda: "host" == 'serverA')
.groupBy('host')
|window()
.period(10s)
.every(10s)
|count('value')
|default()
.field('extraemail','[email protected]')
.tag('tagemail','[email protected]')
|alert()
.id('kapacitor.{{ .Name }}.{{ index .Tags "host" }}')
.details('''
<b>{{.Message}}</b>
Value: {{ index .Fields "count" }}
<a href="http://graphs.example.com/host/{{index .Tags "host"}}">Details</a>
''')
.info(lambda: "count" > 6.0)
.warn(lambda: "count" > 7.0)
.crit(lambda: "count" > 8.0)
.email()
.to('[email protected]', '[email protected]')
.toTemplates('{{ index .Fields "extraemail" }}')
`,
expected: []*smtptest.Message{
{
Header: mail.Header{
"Mime-Version": []string{"1.0"},
"Content-Type": []string{"text/html; charset=UTF-8"},
"Content-Transfer-Encoding": []string{"quoted-printable"},
"To": []string{"[email protected], [email protected], [email protected]"},
"From": []string{"[email protected]"},
"Subject": []string{"kapacitor.cpu.serverA is CRITICAL"},
},
Body: `
<b>kapacitor.cpu.serverA is CRITICAL</b>
Value: 10
<a href=3D"http://graphs.example.com/host/serverA">Details</a>
`,
}},
},
{
Header: mail.Header{
"Mime-Version": []string{"1.0"},
"Content-Type": []string{"text/html; charset=UTF-8"},
"Content-Transfer-Encoding": []string{"quoted-printable"},
"To": []string{"[email protected], [email protected]"},
"From": []string{"[email protected]"},
"Subject": []string{"kapacitor.cpu.serverA is CRITICAL"},
},
Body: `
name: "emails directly and in tag",
script: `stream
|from()
.measurement('cpu')
.where(lambda: "host" == 'serverA')
.groupBy('host')
|window()
.period(10s)
.every(10s)
|count('value')
|default()
.field('extraemail','[email protected]')
.tag('tagemail','[email protected]')
|alert()
.id('kapacitor.{{ .Name }}.{{ index .Tags "host" }}')
.details('''
<b>{{.Message}}</b>
Value: {{ index .Fields "count" }}
<a href="http://graphs.example.com/host/{{index .Tags "host"}}">Details</a>
''')
.info(lambda: "count" > 6.0)
.warn(lambda: "count" > 7.0)
.crit(lambda: "count" > 8.0)
.email()
.to('[email protected]', '[email protected]')
.toTemplates('{{ index .Tags "tagemail" }}')
`,
expected: []*smtptest.Message{
{
Header: mail.Header{
"Mime-Version": []string{"1.0"},
"Content-Type": []string{"text/html; charset=UTF-8"},
"Content-Transfer-Encoding": []string{"quoted-printable"},
"To": []string{"[email protected], [email protected], [email protected]"},
"From": []string{"[email protected]"},
"Subject": []string{"kapacitor.cpu.serverA is CRITICAL"},
},
Body: `
<b>kapacitor.cpu.serverA is CRITICAL</b>
Value: 10
<a href=3D"http://graphs.example.com/host/serverA">Details</a>
`,
},
},
},
}
for i := range cases {
t.Run(cases[i].name, func(t *testing.T) {
script := cases[i].script
expMail := cases[i].expected
smtpServer, err := smtptest.NewServer()
if err != nil {
t.Fatal(err)
}
defer func() {
err := smtpServer.Close()
if err != nil {
t.Fatalf("error in closing smtpService %v", err)
}
}()

smtpServer, err := smtptest.NewServer()
if err != nil {
t.Fatal(err)
}
defer smtpServer.Close()
sc := smtp.Config{
Enabled: true,
Host: smtpServer.Host,
Port: smtpServer.Port,
From: "[email protected]",
}
smtpService := smtp.NewService(sc, diagService.NewSMTPHandler())
if err := smtpService.Open(); err != nil {
t.Fatal(err)
}
defer smtpService.Close()
sc := smtp.Config{
Enabled: true,
Host: smtpServer.Host,
Port: smtpServer.Port,
From: "[email protected]",
}
smtpService := smtp.NewService(sc, diagService.NewSMTPHandler())
if err := smtpService.Open(); err != nil {
t.Fatal(err)
}

tmInit := func(tm *kapacitor.TaskMaster) {
tm.SMTPService = smtpService
}
// make sure its closed
defer func() {
err := smtpService.Close()
if err != nil {
t.Fatalf("error in closing smtpService %v", err)
}
}()

testStreamerNoOutput(t, "TestStream_Alert", script, 13*time.Second, tmInit)
tmInit := func(tm *kapacitor.TaskMaster) {
tm.SMTPService = smtpService
}

// Close both client and server to ensure all message are processed
smtpService.Close()
smtpServer.Close()
testStreamerNoOutput(t, "TestStream_Alert", script, 13*time.Second, tmInit)

errors := smtpServer.Errors()
if got, exp := len(errors), 0; got != exp {
t.Errorf("unexpected smtp server errors: %v", errors)
}
// Close both client and server to ensure all message are processed
if err := smtpService.Close(); err != nil {
t.Fatalf("error in closing smtpService %v", err)
}
if err := smtpServer.Close(); err != nil {
t.Fatalf("error in closing smtpServer %v", err)
}

msgs := smtpServer.SentMessages()
if got, exp := len(msgs), len(expMail); got != exp {
t.Errorf("unexpected number of messages sent: got %d exp %d", got, exp)
}
for i, exp := range expMail {
got := msgs[i]
if err := exp.Compare(got); err != nil {
t.Errorf("%d %s", i, err)
}
errors := smtpServer.Errors()
if got, exp := len(errors), 0; got != exp {
t.Errorf("unexpected smtp server errors: %v", errors)
}

msgs := smtpServer.SentMessages()
if got, exp := len(msgs), len(expMail); got != exp {
t.Errorf("unexpected number of messages sent: got %d exp %d", got, exp)
}
for i, exp := range expMail {
got := msgs[i]
if err := exp.Compare(got); err != nil {
t.Errorf("%d %s", i, err)
}
}
})
}
}

Expand Down Expand Up @@ -13672,6 +13795,7 @@ func testStreamerNoOutput(
duration time.Duration,
tmInit func(tm *kapacitor.TaskMaster),
) {
t.Helper()
clock, et, replayErr, tm := testStreamer(t, name, script, tmInit)
defer tm.Close()
err := fastForwardTask(clock, et, replayErr, tm, duration)
Expand Down
31 changes: 31 additions & 0 deletions pipeline/alert.go
Original file line number Diff line number Diff line change
Expand Up @@ -796,6 +796,10 @@ type EmailHandler struct {
// List of email recipients.
// tick:ignore
ToList []string `tick:"To" json:"to"`

// ToTemplatesList is the Field or Value from which to grab email addresses
// tick:ignore
ToTemplatesList []string `tick:"ToTemplates" json:"to-templates"`
}

// Define the To addresses for the email alert.
Expand Down Expand Up @@ -826,6 +830,33 @@ func (h *EmailHandler) To(to ...string) *EmailHandler {
return h
}

// Define the To addresses for the email alert.
// Multiple calls append to the existing list of addresses.
// If empty uses the addresses from the configuration.
//
// Example:
// |alert()
// .id('{{ .Name }}')
// // Email subject
// .message('{{ .ID }}:{{ .Level }}')
// //Email body as HTML
// .details('''
//<h1>{{ .ID }}</h1>
//<b>{{ .Message }}</b>
//Value: {{ index .Fields "value" }}
//''')
// .email('[email protected]')
// .toTemplates('[email protected]')
//
// All three email addresses will receive the alert message.
//
// Passing addresses to the `email` property directly or using the `email.to` property is the same.
// tick:property
func (h *EmailHandler) ToTemplates(to ...string) *EmailHandler {
h.ToTemplatesList = append(h.ToTemplatesList, to...)
return h
}

// Execute a command whenever an alert is triggered and pass the alert data over STDIN in JSON format.
// tick:property
func (n *AlertNodeData) Exec(executable string, args ...string) *ExecHandler {
Expand Down
Loading

0 comments on commit 4c2b965

Please sign in to comment.