diff --git a/frontend/src/pages/Settings.vue b/frontend/src/pages/Settings.vue index 0c378147..3de2739d 100644 --- a/frontend/src/pages/Settings.vue +++ b/frontend/src/pages/Settings.vue @@ -25,6 +25,9 @@ {{notifier.title}} ON + + Variables +
Statping Links
@@ -116,6 +119,135 @@ +
+

Notifier Variables

+ You can insert dynamic fields within the notifier payloads for some notifiers. + +

+ Checkout the Service struct and the Failures struct and create variables in golang template format. +

+ +

+ For example, if you have {{"\{\{.Service.Name\}\}"}} it will return the service name. +

+ +

Service Variables

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Additional variables within the Service struct +
VariableTrue Value
{{"\{\{.Service.Id\}\}"}}1
{{"\{\{.Service.Name\}\}"}}Example Service
{{"\{\{.Service.Domain\}\}"}}https://statping.com
{{"\{\{.Service.Port\}\}"}}8080
{{"\{\{.Service.DowntimeAgo\}\}"}}35 minutes ago
{{"\{\{.Service.LastStatusCode\}\}"}}404
{{"\{\{.Service.FailuresLast24Hours\}\}"}}38
+ +

Failure Variables

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Additional variables within the Failures struct +
VariableTrue Value
{{"\{\{.Failure.Issue\}\}"}}Received 404 status code
{{"\{\{.Failure.ErrorCode\}\}"}}404
{{"\{\{.Failure.Service\}\}"}}1
{{"\{\{.Failure.PingTime\}\}"}}12482 (microseconds)
{{"\{\{.Failure.DowntimeAgo\}\}"}}35 minutes ago
{{"\{\{.Failure.CreatedAt\}\}"}}2020-05-02 09:14:43.66381 +0000 UTC
+ +

Core Variables

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Additional variables within the Core struct +
VariableTrue Value
{{"\{\{.Core.Domain\}\}"}}http://localhost:8080
{{"\{\{.Core.Name\}\}"}}Statping Demo
{{"\{\{.Core.Description\}\}"}}Statping will monitor your stuff!
{{"\{\{.Core.Version\}\}"}}v0.90.34
{{"\{\{.Core.Started\}\}"}}2020-05-02 09:14:43.66381 +0000 UTC
+ + +
+
diff --git a/go.mod b/go.mod index 93495c75..f7a30637 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/GeertJohan/go.rice v1.0.0 github.com/ararog/timeago v0.0.0-20160328174124-e9969cf18b8d github.com/dgrijalva/jwt-go v3.2.0+incompatible + github.com/dustin/go-humanize v1.0.0 github.com/fatih/structs v1.1.0 github.com/fsnotify/fsnotify v1.4.9 // indirect github.com/getsentry/sentry-go v0.5.1 @@ -18,6 +19,7 @@ require ( github.com/jinzhu/gorm v1.9.12 github.com/joho/godotenv v1.3.0 github.com/kataras/iris/v12 v12.0.1 + github.com/magiconair/properties v1.8.1 github.com/mattn/go-sqlite3 v2.0.3+incompatible github.com/mitchellh/mapstructure v1.2.2 // indirect github.com/pelletier/go-toml v1.7.0 // indirect diff --git a/go.sum b/go.sum index 8d5246b6..271df335 100755 --- a/go.sum +++ b/go.sum @@ -55,6 +55,7 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumC github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= diff --git a/notifiers/command.go b/notifiers/command.go index f68d3f6d..9a9bb0c2 100644 --- a/notifiers/command.go +++ b/notifiers/command.go @@ -32,7 +32,7 @@ var Command = &commandLine{¬ifications.Notification{ Limits: 60, Form: []notifications.NotificationForm{{ Type: "text", - Title: "Shell or Bash", + Title: "Executable Path", Placeholder: "/usr/bin/curl", DbField: "host", SmallText: "You can use '/bin/sh', '/bin/bash', '/usr/bin/curl' or an absolute path for an application.", @@ -41,13 +41,13 @@ var Command = &commandLine{¬ifications.Notification{ Title: "Command to Run on OnSuccess", Placeholder: "http://localhost:8080/health", DbField: "var1", - SmallText: "This Command will run when a service is receiving a Successful event.", + SmallText: "Accepts Variables This Command will run when a service is receiving a Successful event.", }, { Type: "text", Title: "Command to Run on OnFailure", Placeholder: "http://localhost:8080/health", DbField: "var2", - SmallText: "This Command will run when a service is receiving a Failing event.", + SmallText: "Accepts Variables This Command will run when a service is receiving a Failing event.", }}}, } diff --git a/notifiers/email.go b/notifiers/email.go index 5caa99ce..7c6f6129 100644 --- a/notifiers/email.go +++ b/notifiers/email.go @@ -155,25 +155,21 @@ type emailOutgoing struct { Subject string Template string From string - Data emailData + Data replacer Source string Sent bool } -type emailData struct { - Service services.Service - Failure failures.Failure -} - // OnFailure will trigger failing service func (e *emailer) OnFailure(s *services.Service, f *failures.Failure) error { + subject := fmt.Sprintf("Service %s is Offline", s.Name) email := &emailOutgoing{ To: e.Var2, - Subject: fmt.Sprintf("Service %v is Failing", s.Name), + Subject: subject, Template: mainEmailTemplate, - Data: emailData{ - Service: *s, - Failure: *f, + Data: replacer{ + Service: s, + Failure: f, }, From: e.Var1, } @@ -182,14 +178,14 @@ func (e *emailer) OnFailure(s *services.Service, f *failures.Failure) error { // OnSuccess will trigger successful service func (e *emailer) OnSuccess(s *services.Service) error { - msg := s.DownText + subject := fmt.Sprintf("Service %s is Back Online", s.Name) email := &emailOutgoing{ To: e.Var2, - Subject: msg, + Subject: subject, Template: mainEmailTemplate, - Data: emailData{ - Service: *s, - Failure: failures.Failure{}, + Data: replacer{ + Service: s, + Failure: &failures.Failure{}, }, From: e.Var1, } @@ -217,9 +213,9 @@ func (e *emailer) OnTest() (string, error) { To: e.Var2, Subject: subject, Template: mainEmailTemplate, - Data: emailData{ - Service: testService, - Failure: failures.Failure{}, + Data: replacer{ + Service: &testService, + Failure: &failures.Failure{}, }, From: e.Var1, } @@ -239,7 +235,7 @@ func (e *emailer) dialSend(email *emailOutgoing) error { m.SetHeader("From", email.From) m.SetHeader("To", email.To) m.SetHeader("Subject", email.Subject) - m.SetBody("text/html", utils.ReplaceTemplate(email.Template, email.Data)) + m.SetBody("text/html", ReplaceTemplate(email.Template, email.Data)) if err := mailer.DialAndSend(m); err != nil { utils.Log.Errorln(fmt.Sprintf("email '%v' sent to: %v (size: %v) %v", email.Subject, email.To, len([]byte(email.Source)), err)) diff --git a/notifiers/notifiers.go b/notifiers/notifiers.go index 69b0df49..81f74f69 100644 --- a/notifiers/notifiers.go +++ b/notifiers/notifiers.go @@ -1,15 +1,24 @@ package notifiers import ( + "bytes" + "github.com/statping/statping/types/core" "github.com/statping/statping/types/failures" "github.com/statping/statping/types/null" "github.com/statping/statping/types/services" "github.com/statping/statping/utils" + "html/template" "time" ) var log = utils.Log.WithField("type", "notifier") +type replacer struct { + Core *core.Core + Service *services.Service + Failure *failures.Failure +} + func InitNotifiers() { Add( slacker, @@ -25,6 +34,21 @@ func InitNotifiers() { ) } +func ReplaceTemplate(tmpl string, data replacer) string { + buf := new(bytes.Buffer) + tmp, err := template.New("replacement").Parse(tmpl) + if err != nil { + log.Error(err) + return err.Error() + } + err = tmp.Execute(buf, data) + if err != nil { + log.Error(err) + return err.Error() + } + return buf.String() +} + func Add(notifs ...services.ServiceNotifier) { for _, n := range notifs { services.AddNotifier(n) @@ -34,19 +58,8 @@ func Add(notifs ...services.ServiceNotifier) { } } -func ToMap(srv *services.Service, f *failures.Failure) map[string]interface{} { - m := make(map[string]interface{}) - m["Service"] = srv - m["Failure"] = f - return m -} - func ReplaceVars(input string, s *services.Service, f *failures.Failure) string { - input = utils.ReplaceTemplate(input, s) - if f != nil { - input = utils.ReplaceTemplate(input, f) - } - return input + return ReplaceTemplate(input, replacer{Service: s, Failure: f, Core: core.App}) } var exampleService = &services.Service{ @@ -86,6 +99,7 @@ var exampleService = &services.Service{ LastOffline: utils.Now().Add(-10 * time.Minute), SecondsOnline: 4500, SecondsOffline: 300, + Redirect: null.NewNullBool(true), } var exampleFailure = &failures.Failure{ diff --git a/notifiers/notifiers_test.go b/notifiers/notifiers_test.go new file mode 100644 index 00000000..3bd87c4f --- /dev/null +++ b/notifiers/notifiers_test.go @@ -0,0 +1,16 @@ +package notifiers + +import ( + "github.com/magiconair/properties/assert" + "testing" +) + +func TestReplaceTemplate(t *testing.T) { + temp := `{"id":{{.Service.Id}},"name":"{{.Service.Name}}"}` + replaced := ReplaceTemplate(temp, replacer{Service: exampleService}) + assert.Equal(t, `{"id":1,"name":"Statping"}`, replaced) + + temp = `{"id":{{.Service.Id}},"name":"{{.Service.Name}}","downtime":"{{.Service.DowntimeAgo}}","failure":"{{.Failure.Issue}}"}` + replaced = ReplaceTemplate(temp, replacer{Service: exampleService, Failure: exampleFailure}) + assert.Equal(t, `{"id":1,"name":"Statping","failure":"HTTP returned a 500 status code"}`, replaced) +} diff --git a/notifiers/pushover.go b/notifiers/pushover.go index c88d9f87..ae85eb13 100644 --- a/notifiers/pushover.go +++ b/notifiers/pushover.go @@ -68,21 +68,21 @@ func (t *pushover) sendMessage(message string) (string, error) { // OnFailure will trigger failing service func (t *pushover) OnFailure(s *services.Service, f *failures.Failure) error { - msg := fmt.Sprintf("Your service '%v' is currently offline!", s.Name) + msg := fmt.Sprintf("Your service '%s' is currently offline!", s.Name) _, err := t.sendMessage(msg) return err } // OnSuccess will trigger successful service func (t *pushover) OnSuccess(s *services.Service) error { - msg := fmt.Sprintf("Your service '%v' is currently online!", s.Name) + msg := fmt.Sprintf("Your service '%s' is currently online!", s.Name) _, err := t.sendMessage(msg) return err } // OnTest will test the Pushover SMS messaging func (t *pushover) OnTest() (string, error) { - msg := fmt.Sprintf("Testing the Pushover Notifier") + msg := fmt.Sprintf("Testing the Pushover Notifier, Your service '%s' is currently offline! Error: %s", exampleService.Name, exampleFailure.Issue) content, err := t.sendMessage(msg) return content, err } diff --git a/notifiers/slack.go b/notifiers/slack.go index 07fe0196..2eb776cb 100644 --- a/notifiers/slack.go +++ b/notifiers/slack.go @@ -16,8 +16,8 @@ var _ notifier.Notifier = (*slack)(nil) const ( slackMethod = "slack" - failingTemplate = `{ "attachments": [ { "fallback": "Service {{.Service.Name}} - is currently failing", "text": "Your Statping service <{{.Service.Domain}}|{{.Service.Name}}> has just received a Failure notification based on your expected results. {{.Service.Name}} responded with a HTTP Status code of {{.Service.LastStatusCode}}.", "fields": [ { "title": "Expected Status Code", "value": "{{.Service.ExpectedStatus}}", "short": true }, { "title": "Received Status Code", "value": "{{.Service.LastStatusCode}}", "short": true } ,{ "title": "Error Message", "value": "{{.Failure.Issue}}", "short": false } ], "color": "#FF0000", "thumb_url": "https://statping.com", "footer": "Statping", "footer_icon": "https://img.cjx.io/statuplogo32.png" } ] }` - successTemplate = `{ "attachments": [ { "fallback": "Service {{.Service.Name}} - is now back online", "text": "Your Statping service <{{.Service.Domain}}|{{.Service.Name}}> is now back online and meets your expected responses.", "color": "#00FF00", "thumb_url": "https://statping.com", "footer": "Statping", "footer_icon": "https://img.cjx.io/statuplogo32.png" } ] }` + failingTemplate = `{ "blocks": [ { "type": "section", "text": { "type": "mrkdwn", "text": ":warning: The service {{.Service.Name}} is currently offline! :warning:" } }, { "type": "divider" }, { "type": "section", "fields": [ { "type": "mrkdwn", "text": "*Service:*\n{{.Service.Name}}" }, { "type": "mrkdwn", "text": "*URL:*\n{{.Service.Domain}}" }, { "type": "mrkdwn", "text": "*Status Code:*\n{{.Service.LastStatusCode}}" }, { "type": "mrkdwn", "text": "*When:*\n{{.Failure.CreatedAt}}" }, { "type": "mrkdwn", "text": "*Downtime:*\n{{.Service.DowntimeAgo}}" }, { "type": "plain_text", "text": "*Error:*\n{{.Failure.Issue}}" } ] }, { "type": "divider" }, { "type": "actions", "elements": [ { "type": "button", "text": { "type": "plain_text", "text": "View Offline Service", "emoji": true }, "style": "danger", "url": "{{.Core.Domain}}/service/{{.Service.Id}}" }, { "type": "button", "text": { "type": "plain_text", "text": "Go to Statping", "emoji": true }, "url": "{{.Core.Domain}}" } ] } ] }` + successTemplate = `{ "blocks": [ { "type": "section", "text": { "type": "mrkdwn", "text": "The service {{.Service.Name}} is back online." } }, { "type": "actions", "elements": [ { "type": "button", "text": { "type": "plain_text", "text": "View Service", "emoji": true }, "style": "primary", "url": "{{.Core.Domain}}/service/{{.Service.Id}}" }, { "type": "button", "text": { "type": "plain_text", "text": "Go to Statping", "emoji": true }, "url": "{{.Core.Domain}}" } ] } ] }` ) type slack struct { @@ -59,7 +59,8 @@ func (s *slack) sendSlack(msg string) error { } func (s *slack) OnTest() (string, error) { - contents, resp, err := utils.HttpRequest(s.Host, "POST", "application/json", nil, bytes.NewBuffer([]byte(`{"text":"testing message"}`)), time.Duration(10*time.Second), true) + testMsg := ReplaceVars(failingTemplate, exampleService, exampleFailure) + contents, resp, err := utils.HttpRequest(s.Host, "POST", "application/json", nil, bytes.NewBuffer([]byte(testMsg)), time.Duration(10*time.Second), true) if err != nil { return "", err } diff --git a/types/notifications/methods.go b/types/notifications/methods.go index cec62d52..6b2e527d 100644 --- a/types/notifications/methods.go +++ b/types/notifications/methods.go @@ -45,6 +45,7 @@ func (n *Notification) CanSend() bool { //fmt.Println("Last sent: ", n.lastSent.String()) //fmt.Println("Last count: ", n.lastSentCount) + //fmt.Println("Limits: ", n.Limits) //fmt.Println("Last sent before now: ", n.lastSent.Add(60*time.Second).Before(utils.Now())) // the last sent notification was past 1 minute (limit per minute) diff --git a/types/services/failures.go b/types/services/failures.go index fe5badcf..e524d158 100644 --- a/types/services/failures.go +++ b/types/services/failures.go @@ -2,6 +2,7 @@ package services import ( "fmt" + humanize "github.com/dustin/go-humanize" "github.com/statping/statping/types/failures" "strings" "time" @@ -20,6 +21,14 @@ func (s *Service) FailuresSince(t time.Time) failures.Failurer { return fails } +func (s *Service) DowntimeAgo() string { + last := s.LastOnline + if last.IsZero() { + return "Never been online" + } + return humanize.Time(last) +} + func (s *Service) DowntimeText() string { last := s.AllFailures().Last() if last == nil { diff --git a/types/services/routine.go b/types/services/routine.go index 90204a98..c7956e76 100644 --- a/types/services/routine.go +++ b/types/services/routine.go @@ -379,7 +379,7 @@ func sendFailure(s *Service, f *failures.Failure) { return } - if s.NotifyAfter == 0 || s.notifyAfterCount > s.NotifyAfter { + if s.notifyAfterCount > s.NotifyAfter { for _, n := range allNotifiers { notif := n.Select() if notif.CanSend() { diff --git a/utils/replacer.go b/utils/replacer.go deleted file mode 100644 index 50d238f2..00000000 --- a/utils/replacer.go +++ /dev/null @@ -1,24 +0,0 @@ -package utils - -import ( - "bytes" - "text/template" -) - -func ReplaceTemplate(tmpl string, data interface{}) string { - buf := new(bytes.Buffer) - - tmp, err := template.New("replacement").Parse(tmpl) - if err != nil { - Log.Error(err) - return err.Error() - } - - err = tmp.Execute(buf, data) - if err != nil { - Log.Error(err) - return err.Error() - } - - return buf.String() -} diff --git a/utils/utils_test.go b/utils/utils_test.go index 207d607b..55200c1a 100644 --- a/utils/utils_test.go +++ b/utils/utils_test.go @@ -40,20 +40,6 @@ func TestCommand(t *testing.T) { assert.Contains(t, out, "statping") } -func TestReplaceTemplate(t *testing.T) { - type Object struct { - Id int64 - String string - Online bool - Example string - } - ex := &Object{ - 1, "this is an example", true, "it should work", - } - result := ReplaceTemplate(`{"id": {{.Id}} }`, ex) - assert.Equal(t, "{\"id\": 1 }", result) -} - func TestToInt(t *testing.T) { assert.Equal(t, int64(55), ToInt("55")) assert.Equal(t, int64(55), ToInt(55))