sending notifications on failure.

pull/75/head
Jason Kulatunga 4 years ago
parent 1246f5bba9
commit 6377a258f6

@ -32,19 +32,12 @@ log:
file: '' #absolute or relative paths allowed, eg. web.log file: '' #absolute or relative paths allowed, eg. web.log
level: INFO level: INFO
# The following commented out sections are a preview of additional configuration options that will be available soon.
#disks:
# include:
# # - /dev/sda
# exclude:
# # - /dev/sdb
#notify: #notify:
# urls: # urls:
# - "discord://token@channel" # - "discord://token@channel"
# - "telegram://token@telegram?channels=channel-1[,channel-2,...]" # - "telegram://token@telegram?channels=channel-1[,channel-2,...]"
# - "pushover://shoutrrr:apiToken@userKey/?devices=device1[,device2, ...]" # - "pushover://shoutrrr:apiToken@userKey/?priority=1&devices=device1[,device2, ...]"
# - "slack://[botname@]token-a/token-b/token-c" # - "slack://[botname@]token-a/token-b/token-c"
# - "smtp://username:password@host:port/?fromAddress=fromAddress&toAddresses=recipient1[,recipient2,...]" # - "smtp://username:password@host:port/?fromAddress=fromAddress&toAddresses=recipient1[,recipient2,...]"
# - "teams://token-a/token-b/token-c" # - "teams://token-a/token-b/token-c"
@ -58,6 +51,15 @@ log:
# - "script:///file/path/on/disk" # - "script:///file/path/on/disk"
# - "https://www.example.com/path" # - "https://www.example.com/path"
# The following commented out sections are a preview of additional configuration options that will be available soon.
#disks:
# include:
# # - /dev/sda
# exclude:
# # - /dev/sdb
#limits: #limits:
# ata: # ata:
# critical: # critical:

@ -7,26 +7,57 @@ import (
"github.com/analogj/go-util/utils" "github.com/analogj/go-util/utils"
"github.com/analogj/scrutiny/webapp/backend/pkg/config" "github.com/analogj/scrutiny/webapp/backend/pkg/config"
"github.com/containrrr/shoutrrr" "github.com/containrrr/shoutrrr"
log "github.com/sirupsen/logrus" shoutrrrTypes "github.com/containrrr/shoutrrr/pkg/types"
"github.com/sirupsen/logrus"
"net/http" "net/http"
"net/url"
"os" "os"
"strings" "strings"
"sync" "sync"
"time" "time"
) )
const NotifyFailureTypeEmailTest = "EmailTest"
const NotifyFailureTypeSmartPrefail = "SmartPreFailure"
const NotifyFailureTypeSmartFailure = "SmartFailure"
const NotifyFailureTypeSmartErrorLog = "SmartErrorLog"
const NotifyFailureTypeSmartSelfTest = "SmartSelfTestLog"
// TODO: include host and/or user label for device.
type Payload struct { type Payload struct {
Mailer string `json:"mailer"` Date string `json:"date"` //populated by Send function.
Subject string `json:"subject"` FailureType string `json:"failure_type"` //EmailTest, SmartFail, ScrutinyFail
Date string `json:"date"` DeviceType string `json:"device_type"` //ATA/SCSI/NVMe
FailureType string `json:"failure_type"` DeviceName string `json:"device_string"` //dev/sda
Device string `json:"device"` DeviceSerial string `json:"device"` //WDDJ324KSO
DeviceType string `json:"device_type"` Test bool `json:"-"` // false
DeviceString string `json:"device_string"` }
Message string `json:"message"`
func (p *Payload) GenerateMessage() string {
//generate a detailed failure message
return fmt.Sprintf("Scrutiny SMART error (%s) detected on device: %s", p.FailureType, p.DeviceName)
}
func (p *Payload) GenerateSubject() string {
//generate a detailed failure message
message := fmt.Sprintf(
`Scrutiny SMART error notification for device: %s
Failure Type: %s
Device Name: %s
Device Serial: %s
Device Type: %s
Date: %s`, p.DeviceName, p.FailureType, p.DeviceName, p.DeviceSerial, p.DeviceType, p.Date)
if p.Test {
message = "TEST NOTIFICATION:\n" + message
}
return message
} }
type Notify struct { type Notify struct {
Logger logrus.FieldLogger
Config config.Interface Config config.Interface
Payload Payload Payload Payload
} }
@ -38,6 +69,7 @@ func (n *Notify) Send() error {
//retrieve list of notification endpoints from config file //retrieve list of notification endpoints from config file
configUrls := n.Config.GetStringSlice("notify.urls") configUrls := n.Config.GetStringSlice("notify.urls")
n.Logger.Debugf("Configured notification services: %v", configUrls)
//remove http:// https:// and script:// prefixed urls //remove http:// https:// and script:// prefixed urls
notifyWebhooks := []string{} notifyWebhooks := []string{}
@ -54,6 +86,10 @@ func (n *Notify) Send() error {
} }
} }
n.Logger.Debugf("Configured scripts: %v", notifyScripts)
n.Logger.Debugf("Configured webhooks: %v", notifyWebhooks)
n.Logger.Debugf("Configured shoutrrr: %v", notifyShoutrrr)
//run all scripts, webhooks and shoutrr commands in parallel //run all scripts, webhooks and shoutrr commands in parallel
var wg sync.WaitGroup var wg sync.WaitGroup
@ -67,12 +103,14 @@ func (n *Notify) Send() error {
wg.Add(1) wg.Add(1)
go n.SendScriptNotification(&wg, notifyScript) go n.SendScriptNotification(&wg, notifyScript)
} }
if len(notifyScripts) > 0 { for _, shoutrrrUrl := range notifyShoutrrr {
wg.Add(1) wg.Add(1)
go n.SendShoutrrrNotification(&wg, notifyShoutrrr) go n.SendShoutrrrNotification(&wg, shoutrrrUrl)
} }
//and wait for completion, error or timeout. //and wait for completion, error or timeout.
n.Logger.Debugf("Main: waiting for notifications to complete.")
//wg.Wait()
if waitTimeout(&wg, time.Minute) { //wait for 1 minute if waitTimeout(&wg, time.Minute) { //wait for 1 minute
fmt.Println("Timed out while sending notifications") fmt.Println("Timed out while sending notifications")
} else { } else {
@ -83,16 +121,16 @@ func (n *Notify) Send() error {
func (n *Notify) SendWebhookNotification(wg *sync.WaitGroup, webhookUrl string) { func (n *Notify) SendWebhookNotification(wg *sync.WaitGroup, webhookUrl string) {
defer wg.Done() defer wg.Done()
log.Infof("Sending Webhook to %s", webhookUrl) n.Logger.Infof("Sending Webhook to %s", webhookUrl)
requestBody, err := json.Marshal(n.Payload) requestBody, err := json.Marshal(n.Payload)
if err != nil { if err != nil {
log.Errorf("An error occurred while sending Webhook to %s: %v", webhookUrl, err) n.Logger.Errorf("An error occurred while sending Webhook to %s: %v", webhookUrl, err)
return return
} }
resp, err := http.Post(webhookUrl, "application/json", bytes.NewBuffer(requestBody)) resp, err := http.Post(webhookUrl, "application/json", bytes.NewBuffer(requestBody))
if err != nil { if err != nil {
log.Errorf("An error occurred while sending Webhook to %s: %v", webhookUrl, err) n.Logger.Errorf("An error occurred while sending Webhook to %s: %v", webhookUrl, err)
return return
} }
defer resp.Body.Close() defer resp.Body.Close()
@ -104,45 +142,97 @@ func (n *Notify) SendScriptNotification(wg *sync.WaitGroup, scriptUrl string) {
//check if the script exists. //check if the script exists.
scriptPath := strings.TrimPrefix(scriptUrl, "script://") scriptPath := strings.TrimPrefix(scriptUrl, "script://")
log.Infof("Executing Script %s", scriptPath) n.Logger.Infof("Executing Script %s", scriptPath)
if !utils.FileExists(scriptPath) { if !utils.FileExists(scriptPath) {
log.Errorf("Script does not exist: %s", scriptPath) n.Logger.Errorf("Script does not exist: %s", scriptPath)
return return
} }
copyEnv := os.Environ() copyEnv := os.Environ()
copyEnv = append(copyEnv, fmt.Sprintf("SCRUTINY_MAILER=%s", n.Payload.Mailer)) copyEnv = append(copyEnv, fmt.Sprintf("SCRUTINY_SUBJECT=%s", n.Payload.GenerateSubject()))
copyEnv = append(copyEnv, fmt.Sprintf("SCRUTINY_SUBJECT=%s", n.Payload.Subject))
copyEnv = append(copyEnv, fmt.Sprintf("SCRUTINY_DATE=%s", n.Payload.Date)) copyEnv = append(copyEnv, fmt.Sprintf("SCRUTINY_DATE=%s", n.Payload.Date))
copyEnv = append(copyEnv, fmt.Sprintf("SCRUTINY_FAILURE_TYPE=%s", n.Payload.FailureType)) copyEnv = append(copyEnv, fmt.Sprintf("SCRUTINY_FAILURE_TYPE=%s", n.Payload.FailureType))
copyEnv = append(copyEnv, fmt.Sprintf("SCRUTINY_DEVICE=%s", n.Payload.Device)) copyEnv = append(copyEnv, fmt.Sprintf("SCRUTINY_DEVICE_NAME=%s", n.Payload.DeviceName))
copyEnv = append(copyEnv, fmt.Sprintf("SCRUTINY_DEVICE_TYPE=%s", n.Payload.DeviceType)) copyEnv = append(copyEnv, fmt.Sprintf("SCRUTINY_DEVICE_TYPE=%s", n.Payload.DeviceType))
copyEnv = append(copyEnv, fmt.Sprintf("SCRUTINY_DEVICE_STRING=%s", n.Payload.DeviceString)) copyEnv = append(copyEnv, fmt.Sprintf("SCRUTINY_DEVICE_SERIAL=%s", n.Payload.DeviceSerial))
copyEnv = append(copyEnv, fmt.Sprintf("SCRUTINY_MESSAGE=%s", n.Payload.Message)) copyEnv = append(copyEnv, fmt.Sprintf("SCRUTINY_MESSAGE=%s", n.Payload.GenerateMessage()))
err := utils.CmdExec(scriptPath, []string{}, "", copyEnv, "") err := utils.CmdExec(scriptPath, []string{}, "", copyEnv, "")
if err != nil { if err != nil {
log.Errorf("An error occurred while executing script %s: %v", scriptPath, err) n.Logger.Errorf("An error occurred while executing script %s: %v", scriptPath, err)
} }
return return
} }
func (n *Notify) SendShoutrrrNotification(wg *sync.WaitGroup, shoutrrrUrls []string) { func (n *Notify) SendShoutrrrNotification(wg *sync.WaitGroup, shoutrrrUrl string) {
log.Infof("Sending notifications to %v", shoutrrrUrls)
fmt.Printf("Sending Notifications to %v", shoutrrrUrl)
n.Logger.Infof("Sending notifications to %v", shoutrrrUrl)
defer wg.Done() defer wg.Done()
sender, err := shoutrrr.CreateSender(shoutrrrUrls...) sender, err := shoutrrr.CreateSender(shoutrrrUrl)
if err != nil { if err != nil {
log.Errorf("An error occurred while sending notifications %v: %v", shoutrrrUrls, err) n.Logger.Errorf("An error occurred while sending notifications %v: %v", shoutrrrUrl, err)
return return
} }
errs := sender.Send(n.Payload.Subject, nil) //structs.Map(n.Payload).()) //sender.SetLogger(n.Logger.)
serviceName, params, err := n.GenShoutrrrNotificationParams(shoutrrrUrl)
n.Logger.Debug("notification data for %s: (%s)\n%v", serviceName, shoutrrrUrl, params)
if err != nil {
n.Logger.Errorf("An error occurred occurred while generating notification payload for %s:\n %v", serviceName, shoutrrrUrl, err)
}
errs := sender.Send(n.Payload.GenerateMessage(), params)
if len(errs) > 0 { if len(errs) > 0 {
log.Errorf("One or more errors occurred occurred while sending notifications %v:\n %v", shoutrrrUrls, errs) n.Logger.Errorf("One or more errors occurred occurred while sending notifications for %s:\n %v", shoutrrrUrl, errs)
for _, err := range errs {
n.Logger.Error(err)
}
} }
} }
func (n *Notify) GenShoutrrrNotificationParams(shoutrrrUrl string) (string, *shoutrrrTypes.Params, error) {
serviceURL, err := url.Parse(shoutrrrUrl)
if err != nil {
return "", nil, err
}
serviceName := serviceURL.Scheme
params := &shoutrrrTypes.Params{}
logoUrl := "https://raw.githubusercontent.com/AnalogJ/scrutiny/master/webapp/frontend/src/ms-icon-144x144.png"
subject := n.Payload.GenerateSubject()
switch serviceName {
// no params supported for these services
case "discord", "hangouts", "ifttt", "mattermost", "teams":
break
case "gotify":
(*params)["title"] = subject
case "join":
(*params)["title"] = subject
(*params)["icon"] = logoUrl
case "pushbullet":
(*params)["title"] = subject
case "pushover":
(*params)["subject"] = subject
case "slack":
(*params)["title"] = subject
(*params)["thumb_url"] = logoUrl
case "smtp":
(*params)["subject"] = subject
case "standard":
(*params)["subject"] = subject
case "telegram":
(*params)["subject"] = subject
case "zulip":
(*params)["topic"] = subject
}
return serviceName, params, nil
}
//utility functions //utility functions
// waitTimeout waits for the waitgroup for the specified max timeout. // waitTimeout waits for the waitgroup for the specified max timeout.
// Returns true if waiting timed out. // Returns true if waiting timed out.

@ -1,14 +1,12 @@
package handler package handler
import ( import (
"fmt"
"github.com/analogj/scrutiny/webapp/backend/pkg/config" "github.com/analogj/scrutiny/webapp/backend/pkg/config"
dbModels "github.com/analogj/scrutiny/webapp/backend/pkg/models/db" dbModels "github.com/analogj/scrutiny/webapp/backend/pkg/models/db"
"github.com/analogj/scrutiny/webapp/backend/pkg/notify" "github.com/analogj/scrutiny/webapp/backend/pkg/notify"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"net/http" "net/http"
"os"
) )
// Send test notification // Send test notification
@ -17,15 +15,14 @@ func SendTestNotification(c *gin.Context) {
logger := c.MustGet("LOGGER").(logrus.FieldLogger) logger := c.MustGet("LOGGER").(logrus.FieldLogger)
testNotify := notify.Notify{ testNotify := notify.Notify{
Logger: logger,
Config: appConfig, Config: appConfig,
Payload: notify.Payload{ Payload: notify.Payload{
Mailer: os.Args[0],
Subject: fmt.Sprintf("Scrutiny SMART error (EmailTest) detected on disk: XXXXX"),
FailureType: "EmailTest", FailureType: "EmailTest",
Device: "/dev/sda", DeviceSerial: "FAKEWDDJ324KSO",
DeviceType: "ata", DeviceType: dbModels.DeviceProtocolAta,
DeviceString: "/dev/sda", DeviceName: "/dev/sda",
Message: "TEST EMAIL from smartd for device: /dev/sda", Test: true,
}, },
} }
err := testNotify.Send() err := testNotify.Send()

@ -1,8 +1,10 @@
package handler package handler
import ( import (
"github.com/analogj/scrutiny/webapp/backend/pkg/config"
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector" "github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
dbModels "github.com/analogj/scrutiny/webapp/backend/pkg/models/db" dbModels "github.com/analogj/scrutiny/webapp/backend/pkg/models/db"
"github.com/analogj/scrutiny/webapp/backend/pkg/notify"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/jinzhu/gorm" "github.com/jinzhu/gorm"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@ -12,6 +14,7 @@ import (
func UploadDeviceMetrics(c *gin.Context) { func UploadDeviceMetrics(c *gin.Context) {
db := c.MustGet("DB").(*gorm.DB) db := c.MustGet("DB").(*gorm.DB)
logger := c.MustGet("LOGGER").(logrus.FieldLogger) logger := c.MustGet("LOGGER").(logrus.FieldLogger)
appConfig := c.MustGet("CONFIG").(config.Interface)
var collectorSmartData collector.SmartInfo var collectorSmartData collector.SmartInfo
err := c.BindJSON(&collectorSmartData) err := c.BindJSON(&collectorSmartData)
@ -45,5 +48,21 @@ func UploadDeviceMetrics(c *gin.Context) {
return return
} }
//check for error
if deviceSmartData.SmartStatus == dbModels.SmartStatusFailed {
//send notifications
testNotify := notify.Notify{
Config: appConfig,
Payload: notify.Payload{
FailureType: notify.NotifyFailureTypeSmartFailure,
DeviceName: device.DeviceName,
DeviceType: device.DeviceProtocol,
DeviceSerial: device.SerialNumber,
Test: false,
},
}
_ = testNotify.Send() //we ignore error message when sending notifications.
}
c.JSON(http.StatusOK, gin.H{"success": true}) c.JSON(http.StatusOK, gin.H{"success": true})
} }

Loading…
Cancel
Save