Compare commits

...

23 Commits

Author SHA1 Message Date
Norwin 8e1cf7ed38 fixes
- improved log levels
- less error prone float string parsing
- TODOs for more & better measurement_faulty checks
- go fmt
5 years ago
Norwin 17c80cecab allow filtering via box attributes in `check|watch all` 5 years ago
Norwin 83fb1e7d76 replace version command with flag 5 years ago
Norwin e3df49f8bf fix error handling in osem api client 5 years ago
Norwin d4c499df38 v1.3.0 5 years ago
Norwin e9ddf63dad add SlackNotifier 5 years ago
Norwin 528c9122f2 fix notification retry logic 5 years ago
Norwin b29081550b udpate docs 5 years ago
Norwin 2c2508c471 add commands to check /all/ boxes, working towards #4
The current implementation is quite limited, as we need to fetch each box
individually. Also, all notifications are sent to the default recipient.
Both issues could be resolved when integrating directly with the DB.
5 years ago
Norwin 5abdd0debb improve box fetch performance
by reusing a http/2 connection for all boxes to check
5 years ago
Norwin 3547d3e0c1 log result summary of all checks 5 years ago
Norwin 43aeb83069 improve notifier performance 5 years ago
Norwin 331852b56c v1.2.0 5 years ago
Norwin 287f26a37c refactor ComposeNotification() 5 years ago
Norwin 3a48d3ae5a refactor notifier config validation 5 years ago
Norwin a9659de229 add XmppNotifier 5 years ago
Norwin fa871f6467 properly set time in email notifications 5 years ago
Norwin 97db2d0b9e regression: only run checks for specified target 6 years ago
Norwin 17c66b5100 improve notification layout
new and resolved issues in separate lists
6 years ago
Norwin d6738691b4 update docs 6 years ago
Norwin 794ea5369d Merge branch 'develop' 6 years ago
Norwin 50f4184139 try to do redirect for gh-pages 6 years ago
Norwin 35648ed30f Set theme jekyll-theme-hacker 6 years ago

@ -42,6 +42,8 @@ Run `osem_notify help config` for details and an example configuration.
`transport` | `options`
------------|------------
`email` | `recipients`: list of email addresses
`slack` | -
`xmpp` | `recipients`: list of JIDs
Want more? [add it](#contribute)!

@ -5,7 +5,13 @@ import (
)
func init() {
checkAllCmd.PersistentFlags().StringVarP(&date, "date", "", "", "filter boxes by date AND phenomenon")
checkAllCmd.PersistentFlags().StringVarP(&exposure, "exposure", "", "", "filter boxes by exposure")
checkAllCmd.PersistentFlags().StringVarP(&grouptag, "grouptag", "", "", "filter boxes by grouptag")
checkAllCmd.PersistentFlags().StringVarP(&model, "model", "", "", "filter boxes by model")
checkAllCmd.PersistentFlags().StringVarP(&phenomenon, "phenomenon", "", "", "filter boxes by phenomenon AND date")
checkCmd.AddCommand(checkBoxCmd)
checkCmd.AddCommand(checkAllCmd)
rootCmd.AddCommand(checkCmd)
}
@ -26,3 +32,15 @@ var checkBoxCmd = &cobra.Command{
return checkAndNotify(args)
},
}
var checkAllCmd = &cobra.Command{
Use: "all",
Short: "one-off check on all boxes registered on the opensensemap instance",
RunE: func(cmd *cobra.Command, args []string) error {
cmd.SilenceUsage = true
// no flag validation, as the API already does a good job at that
return checkAndNotifyAll(parseBoxFilters())
},
}

@ -51,11 +51,10 @@ var debugCacheCmd = &cobra.Command{
},
}
var debugNotificationsCmd = &cobra.Command{
Use: "notifications",
Short: "Verify that notifications are working",
Long: `osem_notify debug notifications sends a test notification according
Long: `osem_notify debug notifications sends a test notification according
to healthchecks.default.notifications.options as defined in the config file`,
RunE: func(cmd *cobra.Command, args []string) error {
defaultNotifyConf := &core.NotifyConfig{}
@ -66,11 +65,11 @@ to healthchecks.default.notifications.options as defined in the config file`,
for transport, notifier := range core.Notifiers {
notLog := log.WithField("transport", transport)
opts := defaultNotifyConf.Notifications.Options
notLog.Infof("testing notifer %s with options %v", transport, opts)
opts := defaultNotifyConf.Notifications
notLog.Infof("testing notifer %s with options %v", transport, opts.Options)
n, err := notifier.New(opts)
if err != nil {
notLog.Warnf("could not initialize %s notifier. configuration might be missing?", transport)
notLog.Warnf("could not initialize %s notifier: %s", transport, err)
continue
}

@ -5,6 +5,7 @@ import (
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
// "github.com/spf13/cobra/doc"
"github.com/spf13/viper"
"github.com/noerw/osem_notify/utils"
@ -23,32 +24,32 @@ var configHelpCmd = &cobra.Command{
> Example configuration:
healthchecks:
# override default health checks for all boxes
default:
notifications:
transport: email
options:
recipients:
- fridolina@example.com
events:
- type: "measurement_age"
target: "all" # all sensors
threshold: "15m" # any duration
- type: "measurement_faulty"
target: "all"
threshold: ""
# set health checks per box
593bcd656ccf3b0011791f5a:
notifications:
options:
recipients:
- ruth.less@example.com
events:
- type: "measurement_max"
target: "593bcd656ccf3b0011791f5b"
threshold: "40"
healthchecks:
# override default health checks for all boxes
default:
notifications:
transport: email
options:
recipients:
- fridolina@example.com
events:
- type: "measurement_age"
target: "all" # all sensors
threshold: "15m" # any duration
- type: "measurement_faulty"
target: "all"
threshold: ""
# set health checks per box
593bcd656ccf3b0011791f5a:
notifications:
options:
recipients:
- ruth.less@example.com
events:
- type: "measurement_max"
target: "593bcd656ccf3b0011791f5b"
threshold: "40"
# only needed when sending notifications via email
email:
@ -58,12 +59,25 @@ var configHelpCmd = &cobra.Command{
pass: bar
from: hildegunst@example.com
# only needed when sending notifications via Slack
slack:
webhook: https://hooks.slack.com/services/T030YPW07/xxxxxxx/xxxxxxxxxxxxxxxxxxxxxx
# only needed when sending notifications via XMPP
xmpp:
host: jabber.example.com:5222
user: foo@jabber.example.com
pass: bar
startls: true
> possible values for healthchecks.*.notifications:
transport | options
----------|-------------------------------------
email | recipients: list of email addresses
slack | -
xmpp | recipients: list of JIDs
> possible values for healthchecks.*.events[]:
@ -87,9 +101,10 @@ var configHelpCmd = &cobra.Command{
}
var rootCmd = &cobra.Command{
Use: "osem_notify",
Short: "Root command displaying help",
Long: "Run healthchecks and send notifications for boxes on opensensemap.org",
Use: "osem_notify",
Short: "Root command displaying help",
Long: "Run healthchecks and send notifications for boxes on opensensemap.org",
Version: "1.3.0",
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
// set up logger
log.SetOutput(os.Stdout)

@ -1,26 +0,0 @@
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
// TODO: insert automatically via build step?
const (
VERSION = "1.0.7"
BUILDDATE = "2018-06-26T00:59:00+02"
)
func init() {
rootCmd.AddCommand(versionCmd)
}
var versionCmd = &cobra.Command{
Use: "version",
Short: "Get build and version information",
Long: "osem_notify version returns its build and version information",
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("%s %s", VERSION, BUILDDATE)
},
}

@ -14,10 +14,16 @@ func init() {
watchInterval int
)
watchAllCmd.PersistentFlags().StringVarP(&date, "date", "", "", "filter boxes by date")
watchAllCmd.PersistentFlags().StringVarP(&exposure, "exposure", "", "", "filter boxes by exposure")
watchAllCmd.PersistentFlags().StringVarP(&grouptag, "grouptag", "", "", "filter boxes by grouptag")
watchAllCmd.PersistentFlags().StringVarP(&model, "model", "", "", "filter boxes by model")
watchAllCmd.PersistentFlags().StringVarP(&phenomenon, "phenomenon", "", "", "filter boxes by phenomenon")
watchCmd.PersistentFlags().IntVarP(&watchInterval, "interval", "i", 30, "interval to run checks in minutes")
viper.BindPFlags(watchCmd.PersistentFlags())
watchCmd.AddCommand(watchBoxesCmd)
watchCmd.AddCommand(watchAllCmd)
rootCmd.AddCommand(watchCmd)
}
@ -54,3 +60,28 @@ var watchBoxesCmd = &cobra.Command{
}
},
}
var watchAllCmd = &cobra.Command{
Use: "all",
Short: "watch all boxes registered on the map",
PreRun: func(cmd *cobra.Command, args []string) {
interval := viper.GetDuration("interval") * time.Minute
ticker = time.NewTicker(interval).C
},
RunE: func(cmd *cobra.Command, args []string) error {
cmd.SilenceUsage = true
filters := parseBoxFilters()
err := checkAndNotifyAll(filters)
if err != nil {
return err
}
for {
<-ticker
err = checkAndNotifyAll(filters)
if err != nil {
// we already did retries, so exiting seems appropriate
return err
}
}
},
}

@ -39,24 +39,22 @@ func initConfig() {
}
func validateConfig() {
transport := viper.GetString("defaultHealthchecks.notifications.transport")
if viper.GetBool("notify") && transport == "email" {
if len(viper.GetStringSlice("defaultHealthchecks.notifications.options.recipients")) == 0 {
log.Warn("No recipients set up for transport email")
if viper.GetString("notify") != "" {
if len(viper.GetStringSlice("healthchecks.default.notifications.options.recipients")) == 0 {
log.Warn("No default recipients set up for notifications!")
}
emailRequired := []string{
viper.GetString("email.host"),
viper.GetString("email.port"),
viper.GetString("email.user"),
viper.GetString("email.pass"),
viper.GetString("email.from"),
var conf = &core.TransportConfig{}
if err := viper.UnmarshalKey("healthchecks.default.notifications", conf); err != nil {
log.Error("invalid default notification configuration: ", err)
os.Exit(1)
}
for _, conf := range emailRequired {
if conf == "" {
log.Error("Default transport set as email, but missing email config")
os.Exit(1)
}
// creating a notifier validates its configuration
_, err := core.GetNotifier(conf)
if err != nil {
log.Error(err)
os.Exit(1)
}
}
}
@ -85,8 +83,7 @@ func getNotifyConf(boxID string) (*core.NotifyConfig, error) {
if keyDefined("healthchecks.default.events") {
conf.Events = []core.NotifyEvent{}
}
err := viper.UnmarshalKey("healthchecks.default", conf)
if err != nil {
if err := viper.UnmarshalKey("healthchecks.default", conf); err != nil {
return nil, err
}
@ -94,8 +91,7 @@ func getNotifyConf(boxID string) (*core.NotifyConfig, error) {
if keyDefined("healthchecks." + boxID + ".events") {
conf.Events = []core.NotifyEvent{}
}
err = viper.UnmarshalKey("healthchecks."+boxID, conf)
if err != nil {
if err := viper.UnmarshalKey("healthchecks."+boxID, conf); err != nil {
return nil, err
}

@ -5,6 +5,7 @@ import (
"regexp"
"strings"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/viper"
@ -33,6 +34,26 @@ func BoxIdValidator(cmd *cobra.Command, args []string) error {
return nil
}
func checkAndNotifyAll(filters core.BoxFilters) error {
log.Info("getting list of boxes...")
// fetch all boxes first & extract their IDs
osem := core.NewOsemClient(viper.GetString("api"))
boxes, err := osem.GetAllBoxes(filters)
if err != nil {
return err
}
boxIDs := make([]string, len(*boxes))
for i, box := range *boxes {
boxIDs[i] = box.Id
}
// then check each box individually. we only pass the ID
// and fetch again, because box metadata is different in
// GetAllBoxes and GetBox..
return checkAndNotify(boxIDs)
}
func checkAndNotify(boxIds []string) error {
boxLocalConfig := map[string]*core.NotifyConfig{}
for _, boxID := range boxIds {
@ -43,7 +64,8 @@ func checkAndNotify(boxIds []string) error {
boxLocalConfig[boxID] = c
}
results, err := core.CheckBoxes(boxLocalConfig)
osem := core.NewOsemClient(viper.GetString("api"))
results, err := core.CheckBoxes(boxLocalConfig, osem)
if err != nil {
return err
}
@ -69,3 +91,32 @@ func checkAndNotify(boxIds []string) error {
}
return nil
}
var ( // values are set during cli flag parsing of checkAllCmd & watchAllCmd
date string
exposure string
grouptag string
model string
phenomenon string
)
func parseBoxFilters() core.BoxFilters {
filters := core.BoxFilters{}
if date != "" {
filters.Date = date // TODO: parse date & format as ISO date?
}
if exposure != "" {
filters.Exposure = exposure
}
if grouptag != "" {
filters.Grouptag = grouptag
}
if model != "" {
filters.Model = model
}
if phenomenon != "" {
filters.Phenomenon = phenomenon
}
return filters
}

@ -53,11 +53,14 @@ func (results BoxCheckResults) filterChangedFromCache() BoxCheckResults {
return remaining
}
func updateCache(box *Box, results []CheckResult) error {
func updateCache(box *Box, results []CheckResult) {
for _, result := range results {
key := fmt.Sprintf("watchcache.%s.%s", box.Id, result.EventID())
cache.Set(key+".laststatus", result.Status)
}
}
func writeCache() error {
return cache.WriteConfig()
}

@ -5,7 +5,6 @@ import (
"strings"
log "github.com/sirupsen/logrus"
"github.com/spf13/viper"
)
type BoxCheckResults map[*Box][]CheckResult
@ -23,6 +22,16 @@ func (results BoxCheckResults) Size(statusToCheck []string) int {
}
func (results BoxCheckResults) Log() {
// collect statistics for summary print
boxesSkipped := 0
boxesWithIssues := 0
boxesWithoutIssues := 0
failedChecks := 0
errorsByEvent := map[string]int{}
for event, _ := range checkers {
errorsByEvent[event] = 0
}
for box, boxResults := range results {
boxLog := log.WithFields(log.Fields{
"boxId": box.Id,
@ -40,28 +49,54 @@ func (results BoxCheckResults) Log() {
} else {
resultLog.Warnf("%s: %s", box.Name, r)
countErr++
errorsByEvent[r.Event]++
}
}
if len(boxResults) == 0 {
boxLog.Infof("%s: no checks defined", box.Name)
boxesSkipped++
} else if countErr == 0 {
boxLog.Infof("%s: all is fine!", box.Name)
boxesWithoutIssues++
} else {
// we logged the error(s) already
boxesWithIssues++
failedChecks += countErr
}
}
// print summary
boxesChecked := boxesWithIssues + boxesWithoutIssues
if boxesChecked > 1 {
summaryLog := log.WithFields(log.Fields{
"boxesChecked": boxesChecked,
"boxesSkipped": boxesSkipped, // boxes are also skipped when they never submitted any measurements before!
"boxesOk": boxesWithoutIssues,
"boxesErr": boxesWithIssues,
"failedChecks": failedChecks,
"errorsByEvent": errorsByEvent,
})
summaryLog.Infof(
"check summary: %v of %v checked boxes are fine (%v had no checks)!",
boxesWithoutIssues,
boxesChecked,
boxesSkipped)
}
}
func CheckBoxes(boxLocalConfs map[string]*NotifyConfig) (BoxCheckResults, error) {
log.Debug("Checking notifications for ", len(boxLocalConfs), " box(es)")
func CheckBoxes(boxLocalConfs map[string]*NotifyConfig, osem *OsemClient) (BoxCheckResults, error) {
log.Info("Checking notifications for ", len(boxLocalConfs), " box(es)")
results := BoxCheckResults{}
errs := []string{}
// TODO: check boxes in parallel, capped at 5 at once
// @TODO: check boxes in parallel, capped at 5 at once. and/or rate limit?
for boxId, localConf := range boxLocalConfs {
boxLogger := log.WithField("boxId", boxId)
boxLogger.Info("checking box for events")
boxLogger.Debug("checking box for events")
box, res, err := checkBox(boxId, localConf)
box, res, err := checkBox(boxId, localConf, osem)
if err != nil {
boxLogger.Errorf("could not run checks on box %s: %s", boxId, err)
errs = append(errs, err.Error())
@ -76,9 +111,7 @@ func CheckBoxes(boxLocalConfs map[string]*NotifyConfig) (BoxCheckResults, error)
return results, nil
}
func checkBox(boxId string, defaultConf *NotifyConfig) (*Box, []CheckResult, error) {
osem := NewOsemClient(viper.GetString("api"))
func checkBox(boxId string, defaultConf *NotifyConfig, osem *OsemClient) (*Box, []CheckResult, error) {
// get box data
box, err := osem.GetBox(boxId)
if err != nil {

@ -2,7 +2,8 @@ package core
import (
"fmt"
"strconv"
"github.com/noerw/osem_notify/utils"
)
var checkMeasurementFaulty = checkType{
@ -20,7 +21,7 @@ var checkMeasurementFaulty = checkType{
Status: CheckOk,
}
val, err := strconv.ParseFloat(s.LastMeasurement.Value, 64)
val, err := utils.ParseFloat(s.LastMeasurement.Value)
if err != nil {
return result, err
}
@ -42,8 +43,10 @@ type faultyValue struct {
}
var faultyVals = map[faultyValue]bool{
// @TODO: add UV & light sensor: check for 0 if not sunset based on boxlocation
// @TODO: add BME280 and other sensors..
faultyValue{sensor: "BMP280", val: 0.0}: true,
faultyValue{sensor: "HDC1008", val: 0.0}: true,
faultyValue{sensor: "HDC1008", val: 0.0}: true, // @FIXME: check should be on luftfeuchte only!
faultyValue{sensor: "HDC1008", val: -40}: true,
faultyValue{sensor: "SDS 011", val: 0.0}: true,
faultyValue{sensor: "SDS 011", val: 0.0}: true, // @FIXME: 0.0 seems to be a correct value, need to check over longer periods
}

@ -2,7 +2,8 @@ package core
import (
"fmt"
"strconv"
"github.com/noerw/osem_notify/utils"
)
const (
@ -36,12 +37,12 @@ func validateMeasurementMinMax(e NotifyEvent, s Sensor, b Box) (CheckResult, err
Status: CheckOk,
}
thresh, err := strconv.ParseFloat(e.Threshold, 64)
thresh, err := utils.ParseFloat(e.Threshold)
if err != nil {
return result, err
}
val, err := strconv.ParseFloat(s.LastMeasurement.Value, 64)
val, err := utils.ParseFloat(s.LastMeasurement.Value)
if err != nil {
return result, err
}

@ -55,9 +55,9 @@ func (r CheckResult) EventID() string {
func (r CheckResult) String() string {
if r.Status == CheckOk {
return fmt.Sprintf("%s %s (on sensor %s (%s) with value %s)\n", r.Event, r.Status, r.TargetName, r.Target, r.Value)
return fmt.Sprintf("%s: %s (on sensor %s (%s) with value %s)\n", r.Status, r.Event, r.TargetName, r.Target, r.Value)
} else {
return fmt.Sprintf("%s: %s"+"\n", r.Status, checkers[r.Event].toString(r))
return fmt.Sprintf("%s: %s\n", r.Status, checkers[r.Event].toString(r))
}
}
@ -72,6 +72,10 @@ func (box Box) RunChecks() ([]CheckResult, error) {
continue
}
if event.Target != s.Id && event.Target != eventTargetAll {
continue
}
checker := checkers[event.Type]
if checker.checkFunc == nil {
boxLogger.Warnf("ignoring unknown event type %s", event.Type)
@ -80,7 +84,7 @@ func (box Box) RunChecks() ([]CheckResult, error) {
result, err := checker.checkFunc(event, s, box)
if err != nil {
boxLogger.Errorf("error checking event %s", event.Type)
boxLogger.Errorf("error checking event %s: %v", event.Type, err)
}
results = append(results, result)

@ -4,28 +4,36 @@ import (
"errors"
"fmt"
"net/smtp"
"strings"
"time"
"github.com/spf13/viper"
)
// box config required for the EmailNotifier
// box config required for the EmailNotifier (TransportConfig.Options)
type EmailNotifier struct {
Recipients []string
}
func (n EmailNotifier) New(config interface{}) (AbstractNotifier, error) {
func (n EmailNotifier) New(config TransportConfig) (AbstractNotifier, error) {
// validate transport configuration
// :TransportConfSourceHack @FIXME: dont get these values from viper, as the core package
// should be agnostic of the source of configuration!
requiredConf := []string{"email.user", "email.pass", "email.host", "email.port", "email.from"}
for _, key := range requiredConf {
if viper.GetString(key) == "" {
return nil, fmt.Errorf("Missing configuration key %s", key)
}
}
// assign configuration to the notifier after ensuring the correct type.
// lesson of this project: golang requires us to fuck around with type
// assertions, instead of providing us with proper inheritance.
asserted, ok := config.(EmailNotifier)
asserted, ok := config.Options.(EmailNotifier)
if !ok || asserted.Recipients == nil {
// config did not contain valid options.
// first try fallback: parse result of viper is a map[string]interface{},
// which requires a different assertion change
asserted2, ok := config.(map[string]interface{})
asserted2, ok := config.Options.(map[string]interface{})
if ok {
asserted3, ok := asserted2["recipients"].([]interface{})
if ok {
@ -46,20 +54,8 @@ func (n EmailNotifier) New(config interface{}) (AbstractNotifier, error) {
}, nil
}
func (n EmailNotifier) ComposeNotification(box *Box, checks []CheckResult) Notification {
resultTexts := []string{}
for _, check := range checks {
resultTexts = append(resultTexts, check.String())
}
return Notification{
Subject: fmt.Sprintf("Issues with your box \"%s\" on opensensemap.org!", box.Name),
Body: fmt.Sprintf("A check at %s identified the following issue(s) with your box %s:\n\n%s\n\nYou may visit https://opensensemap.org/explore/%s for more details.\n\n--\nSent automatically by osem_notify (https://github.com/noerw/osem_notify)",
time.Now().Round(time.Minute), box.Name, strings.Join(resultTexts, "\n"), box.Id),
}
}
func (n EmailNotifier) Submit(notification Notification) error {
// :TransportConfSourceHack
auth := smtp.PlainAuth(
"",
viper.GetString("email.user"),
@ -68,7 +64,13 @@ func (n EmailNotifier) Submit(notification Notification) error {
)
from := viper.GetString("email.from")
body := fmt.Sprintf("From: openSenseMap Notifier <%s>\nSubject: %s\nContent-Type: text/plain; charset=\"utf-8\"\n\n%s", from, notification.Subject, notification.Body)
body := fmt.Sprintf(
"From: openSenseMap Notifier <%s>\nDate: %s\nSubject: %s\nContent-Type: text/plain; charset=\"utf-8\"\n\n%s%s",
from,
time.Now().Format(time.RFC1123Z),
notification.Subject,
notification.Body,
"\n\n--\nSent automatically by osem_notify (https://github.com/noerw/osem_notify)")
// Connect to the server, authenticate, set the sender and recipient,
// and send the email all in one step.

@ -0,0 +1,73 @@
package core
import (
"fmt"
"io/ioutil"
"net/http"
"github.com/dghubble/sling"
"github.com/spf13/viper"
)
var slackClient = sling.New().Client(&http.Client{})
var notificationColors = map[string]string{
CheckOk: "#00ff00",
CheckErr: "#ff0000",
}
// slack Notifier has no configuration
type SlackNotifier struct {
webhook string
}
type SlackMessage struct {
Text string `json:"text"`
Username string `json:"username,omitempty`
Attachments []SlackAttachment `json:"attachments,omitempty"`
}
type SlackAttachment struct {
Text string `json:"text"`
Color string `json:"color,omitempty"`
}
func (n SlackNotifier) New(config TransportConfig) (AbstractNotifier, error) {
// validate transport configuration
// :TransportConfSourceHack
baseUrl := viper.GetString("slack.webhook")
if baseUrl == "" {
return nil, fmt.Errorf("Missing configuration key slack.webhook")
}
return SlackNotifier{
webhook: baseUrl,
}, nil
}
func (n SlackNotifier) Submit(notification Notification) error {
message := &SlackMessage{
Username: "osem_notify box healthcheck",
Text: notification.Subject,
Attachments: []SlackAttachment{{notification.Body, notificationColors[notification.Status]}},
}
req, err := slackClient.Post(n.webhook).BodyJSON(message).Request()
if err != nil {
return err
}
c := http.Client{}
res, err2 := c.Do(req)
if err2 != nil {
return err2
}
if res.StatusCode > 200 {
defer res.Body.Close()
body, _ := ioutil.ReadAll(res.Body)
return fmt.Errorf("slack webhook failed: %s", body)
}
return nil
}

@ -0,0 +1,102 @@
package core
import (
"errors"
"fmt"
xmpp "github.com/mattn/go-xmpp"
"github.com/spf13/viper"
)
var xmppClient = &xmpp.Client{} // @Hacky
// box config required for the XmppNotifier (TransportConfig.Options)
type XmppNotifier struct {
Recipients []string
}
func (n XmppNotifier) New(config TransportConfig) (AbstractNotifier, error) {
// validate transport configuration
// :TransportConfSourceHack
requiredConf := []string{"xmpp.user", "xmpp.pass", "xmpp.host", "xmpp.starttls"}
for _, key := range requiredConf {
if viper.GetString(key) == "" {
return nil, fmt.Errorf("Missing configuration key %s", key)
}
}
// establish connection with server once, and share it accross instances
// @Hacky
if xmppClient.JID() == "" {
c, err := connectXmpp()
if err != nil {
return nil, err
}
xmppClient = c
}
// assign configuration to the notifier after ensuring the correct type.
// lesson of this project: golang requires us to fuck around with type
// assertions, instead of providing us with proper inheritance.
asserted, ok := config.Options.(XmppNotifier)
if !ok || asserted.Recipients == nil {
// config did not contain valid options.
// first try fallback: parse result of viper is a map[string]interface{},
// which requires a different assertion change
asserted2, ok := config.Options.(map[string]interface{})
if ok {
asserted3, ok := asserted2["recipients"].([]interface{})
if ok {
asserted = XmppNotifier{Recipients: []string{}}
for _, rec := range asserted3 {
asserted.Recipients = append(asserted.Recipients, rec.(string))
}
}
}
if asserted.Recipients == nil {
return nil, errors.New("Invalid XmppNotifier options")
}
}
return XmppNotifier{
Recipients: asserted.Recipients,
}, nil
}
func (n XmppNotifier) Submit(notification Notification) error {
if xmppClient.JID() == "" {
return fmt.Errorf("xmpp client not correctly initialized!")
}
for _, recipient := range n.Recipients {
_, err := xmppClient.Send(xmpp.Chat{
Remote: recipient,
Subject: notification.Subject,
Text: fmt.Sprintf("%s\n\n%s", notification.Subject, notification.Body),
})
if err != nil {
return err
}
}
return nil
}
func connectXmpp() (*xmpp.Client, error) {
// :TransportConfSourceHack
xmppOpts := xmpp.Options{
Host: viper.GetString("xmpp.host"),
User: viper.GetString("xmpp.user"),
Password: viper.GetString("xmpp.pass"),
Resource: "osem_notify",
}
if viper.GetBool("xmpp.starttls") {
xmppOpts.NoTLS = true
xmppOpts.StartTLS = true
}
return xmppOpts.NewClient()
}

@ -10,15 +10,17 @@ import (
var Notifiers = map[string]AbstractNotifier{
"email": EmailNotifier{},
"slack": SlackNotifier{},
"xmpp": XmppNotifier{},
}
type AbstractNotifier interface {
New(config interface{}) (AbstractNotifier, error)
ComposeNotification(box *Box, checks []CheckResult) Notification
New(config TransportConfig) (AbstractNotifier, error)
Submit(notification Notification) error
}
type Notification struct {
Status string // one of CheckOk | CheckErr
Body string
Subject string
}
@ -26,7 +28,12 @@ type Notification struct {
//////
func (box Box) GetNotifier() (AbstractNotifier, error) {
transport := box.NotifyConf.Notifications.Transport
return GetNotifier(&box.NotifyConf.Notifications)
}
func GetNotifier(config *TransportConfig) (AbstractNotifier, error) {
transport := config.Transport
if transport == "" {
return nil, fmt.Errorf("No notification transport provided")
}
@ -36,7 +43,7 @@ func (box Box) GetNotifier() (AbstractNotifier, error) {
return nil, fmt.Errorf("%s is not a supported notification transport", transport)
}
return notifier.New(box.NotifyConf.Notifications.Options)
return notifier.New(*config)
}
func (results BoxCheckResults) SendNotifications(notifyTypes []string, useCache bool) error {
@ -75,13 +82,14 @@ func (results BoxCheckResults) SendNotifications(notifyTypes []string, useCache
continue
}
notification := notifier.ComposeNotification(box, resultsDue)
notification := ComposeNotification(box, resultsDue)
var submitErr error
submitErr = notifier.Submit(notification)
for retry := 1; submitErr != nil && retry < 3; retry++ {
for retry := 0; submitErr != nil && retry < 2; retry++ {
notifyLog.Warnf("sending notification failed (retry %v): %v", retry, submitErr)
time.Sleep(10 * time.Second)
notifyLog.Infof("trying to submit (retry %v)", retry)
submitErr = notifier.Submit(notification)
}
if submitErr != nil {
notifyLog.Error(submitErr)
@ -93,11 +101,7 @@ func (results BoxCheckResults) SendNotifications(notifyTypes []string, useCache
// update cache (with /all/ changed results to reset status)
if useCache {
notifyLog.Debug("updating cache")
cacheError := updateCache(box, resultsBox)
if cacheError != nil {
notifyLog.Error("could not cache notification results: ", cacheError)
errs = append(errs, cacheError.Error())
}
updateCache(box, resultsBox)
}
if len(resultsDue) != 0 {
@ -105,8 +109,53 @@ func (results BoxCheckResults) SendNotifications(notifyTypes []string, useCache
}
}
// persist changes to cache
if useCache {
err := writeCache()
if err != nil {
log.Error("could not write cache of notification results: ", err)
errs = append(errs, err.Error())
}
}
if len(errs) != 0 {
return fmt.Errorf(strings.Join(errs, "\n"))
}
return nil
}
func ComposeNotification(box *Box, checks []CheckResult) Notification {
errTexts := []string{}
resolvedTexts := []string{}
for _, check := range checks {
if check.Status == CheckErr {
errTexts = append(errTexts, check.String())
} else {
resolvedTexts = append(resolvedTexts, check.String())
}
}
var (
resolved string
resolvedList string
errList string
status string
)
if len(resolvedTexts) != 0 {
resolvedList = fmt.Sprintf("Resolved issue(s):\n\n%s\n\n", strings.Join(resolvedTexts, "\n"))
}
if len(errTexts) != 0 {
errList = fmt.Sprintf("New issue(s):\n\n%s\n\n", strings.Join(errTexts, "\n"))
status = CheckErr
} else {
resolved = "resolved "
status = CheckOk
}
return Notification{
Status: status,
Subject: fmt.Sprintf("Issues %swith your box \"%s\" on opensensemap.org!", resolved, box.Name),
Body: fmt.Sprintf("A check at %s identified the following updates for your box \"%s\":\n\n%s%sYou may visit https://opensensemap.org/explore/%s for more details.",
time.Now().Round(time.Minute), box.Name, errList, resolvedList, box.Id),
}
}

@ -9,10 +9,18 @@ import (
)
type OsemError struct {
Code int `json:"code"`
Code string `json:"code"`
Message string `json:"message"`
}
type BoxFilters struct {
Date string `url:"date,omitempty"`
Exposure string `url:"exposure,omitempty"`
Grouptag string `url:"grouptag,omitempty"`
Model string `url:"model,omitempty"`
Phenomenon string `url:"phenomenon,omitempty"`
}
type OsemClient struct {
sling *sling.Sling
}
@ -26,13 +34,29 @@ func NewOsemClient(endpoint string) *OsemClient {
func (client *OsemClient) GetBox(boxId string) (*Box, error) {
box := &Box{}
fail := &OsemError{}
client.sling.New().Path("boxes/").Path(boxId).Receive(box, fail)
_, err := client.sling.New().Path("boxes/").Path(boxId).Receive(box, fail)
if err != nil {
return nil, err
}
if fail.Message != "" {
return box, errors.New("could not fetch box: " + fail.Message)
}
return box, nil
}
func (client *OsemClient) GetAllBoxes(params BoxFilters) (*[]BoxMinimal, error) {
boxes := &[]BoxMinimal{}
fail := &OsemError{}
_, err := client.sling.New().Path("boxes?minimal=true").QueryStruct(params).Receive(boxes, fail)
if err != nil {
return nil, err
}
if fail.Message != "" {
return boxes, errors.New("could not fetch boxes: " + fail.Message)
}
return boxes, nil
}
type NotifyEvent struct {
Type string `json:"type"`
Target string `json:"target"`
@ -65,3 +89,8 @@ type Box struct {
Sensors []Sensor `json:"sensors"`
NotifyConf *NotifyConfig `json:"healthcheck"`
}
type BoxMinimal struct {
Id string `json:"_id"`
Name string `json:"name"`
}

@ -0,0 +1,6 @@
theme: jekyll-theme-hacker
plugins:
- jekyll-redirect-from
whitelist:
- jekyll-redirect-from

@ -0,0 +1,4 @@
---
redirect_to:
- "osem_notify"
---

@ -18,10 +18,16 @@ osem_notify [flags]
-d, --debug enable verbose logging
-h, --help help for osem_notify
-l, --logformat string log format, can be plain or json (default "plain")
-n, --notify if set, will send out notifications.
Otherwise results are printed to stdout only.
--no-cache send all notifications, ignoring results from previous runs. also don't update the cache.
-n, --notify string If set, will send out notifications for the specified type of check result,
otherwise results are printed to stdout only.
Allowed values are "all", "error", "ok".
You might want to run 'osem_notify debug notifications' first to verify everything works.
Notifications for failing checks are sent only once, and then cached until the issue got
resolved, unless --no-cache is set.
To clear the cache, run 'osem_notify debug cache --clear'.
```
### SEE ALSO
@ -31,4 +37,4 @@ osem_notify [flags]
* [osem_notify version](osem_notify_version.md) - Get build and version information
* [osem_notify watch](osem_notify_watch.md) - Watch boxes for events at an interval
###### Auto generated by spf13/cobra on 26-Jun-2018
###### Auto generated by spf13/cobra on 10-Feb-2019

@ -19,15 +19,22 @@ One-off check for events on boxes
-c, --config string path to config file (default $HOME/.osem_notify.yml)
-d, --debug enable verbose logging
-l, --logformat string log format, can be plain or json (default "plain")
-n, --notify if set, will send out notifications.
Otherwise results are printed to stdout only.
--no-cache send all notifications, ignoring results from previous runs. also don't update the cache.
-n, --notify string If set, will send out notifications for the specified type of check result,
otherwise results are printed to stdout only.
Allowed values are "all", "error", "ok".
You might want to run 'osem_notify debug notifications' first to verify everything works.
Notifications for failing checks are sent only once, and then cached until the issue got
resolved, unless --no-cache is set.
To clear the cache, run 'osem_notify debug cache --clear'.
```
### SEE ALSO
* [osem_notify](osem_notify.md) - Root command displaying help
* [osem_notify check all](osem_notify_check_all.md) - one-off check on all boxes registered on the opensensemap instance
* [osem_notify check boxes](osem_notify_check_boxes.md) - one-off check on one or more box IDs
###### Auto generated by spf13/cobra on 26-Jun-2018
###### Auto generated by spf13/cobra on 10-Feb-2019

@ -0,0 +1,42 @@
## osem_notify check all
one-off check on all boxes registered on the opensensemap instance
### Synopsis
one-off check on all boxes registered on the opensensemap instance
```
osem_notify check all [flags]
```
### Options
```
-h, --help help for all
```
### Options inherited from parent commands
```
-a, --api string openSenseMap API to query against (default "https://api.opensensemap.org")
-c, --config string path to config file (default $HOME/.osem_notify.yml)
-d, --debug enable verbose logging
-l, --logformat string log format, can be plain or json (default "plain")
--no-cache send all notifications, ignoring results from previous runs. also don't update the cache.
-n, --notify string If set, will send out notifications for the specified type of check result,
otherwise results are printed to stdout only.
Allowed values are "all", "error", "ok".
You might want to run 'osem_notify debug notifications' first to verify everything works.
Notifications for failing checks are sent only once, and then cached until the issue got
resolved, unless --no-cache is set.
To clear the cache, run 'osem_notify debug cache --clear'.
```
### SEE ALSO
* [osem_notify check](osem_notify_check.md) - One-off check for events on boxes
###### Auto generated by spf13/cobra on 10-Feb-2019

@ -23,14 +23,20 @@ osem_notify check boxes <boxId> [...<boxIds>] [flags]
-c, --config string path to config file (default $HOME/.osem_notify.yml)
-d, --debug enable verbose logging
-l, --logformat string log format, can be plain or json (default "plain")
-n, --notify if set, will send out notifications.
Otherwise results are printed to stdout only.
--no-cache send all notifications, ignoring results from previous runs. also don't update the cache.
-n, --notify string If set, will send out notifications for the specified type of check result,
otherwise results are printed to stdout only.
Allowed values are "all", "error", "ok".
You might want to run 'osem_notify debug notifications' first to verify everything works.
Notifications for failing checks are sent only once, and then cached until the issue got
resolved, unless --no-cache is set.
To clear the cache, run 'osem_notify debug cache --clear'.
```
### SEE ALSO
* [osem_notify check](osem_notify_check.md) - One-off check for events on boxes
###### Auto generated by spf13/cobra on 26-Jun-2018
###### Auto generated by spf13/cobra on 10-Feb-2019

@ -23,15 +23,22 @@ osem_notify debug [flags]
-c, --config string path to config file (default $HOME/.osem_notify.yml)
-d, --debug enable verbose logging
-l, --logformat string log format, can be plain or json (default "plain")
-n, --notify if set, will send out notifications.
Otherwise results are printed to stdout only.
--no-cache send all notifications, ignoring results from previous runs. also don't update the cache.
-n, --notify string If set, will send out notifications for the specified type of check result,
otherwise results are printed to stdout only.
Allowed values are "all", "error", "ok".
You might want to run 'osem_notify debug notifications' first to verify everything works.
Notifications for failing checks are sent only once, and then cached until the issue got
resolved, unless --no-cache is set.
To clear the cache, run 'osem_notify debug cache --clear'.
```
### SEE ALSO
* [osem_notify](osem_notify.md) - Root command displaying help
* [osem_notify debug cache](osem_notify_debug_cache.md) - Print or clear the notifications cache
* [osem_notify debug notifications](osem_notify_debug_notifications.md) - Verify that notifications are working
###### Auto generated by spf13/cobra on 26-Jun-2018
###### Auto generated by spf13/cobra on 10-Feb-2019

@ -0,0 +1,43 @@
## osem_notify debug cache
Print or clear the notifications cache
### Synopsis
osem_notify debug cache prints the contents of the notifications cache
```
osem_notify debug cache [flags]
```
### Options
```
--clear reset the notifications cache
-h, --help help for cache
```
### Options inherited from parent commands
```
-a, --api string openSenseMap API to query against (default "https://api.opensensemap.org")
-c, --config string path to config file (default $HOME/.osem_notify.yml)
-d, --debug enable verbose logging
-l, --logformat string log format, can be plain or json (default "plain")
--no-cache send all notifications, ignoring results from previous runs. also don't update the cache.
-n, --notify string If set, will send out notifications for the specified type of check result,
otherwise results are printed to stdout only.
Allowed values are "all", "error", "ok".
You might want to run 'osem_notify debug notifications' first to verify everything works.
Notifications for failing checks are sent only once, and then cached until the issue got
resolved, unless --no-cache is set.
To clear the cache, run 'osem_notify debug cache --clear'.
```
### SEE ALSO
* [osem_notify debug](osem_notify_debug.md) - Run some debugging checks on osem_notify itself
###### Auto generated by spf13/cobra on 10-Feb-2019

@ -4,7 +4,8 @@ Verify that notifications are working
### Synopsis
osem_notify debug <feature> tests the functionality of the given feature
osem_notify debug notifications sends a test notification according
to healthchecks.default.notifications.options as defined in the config file
```
osem_notify debug notifications [flags]
@ -23,14 +24,20 @@ osem_notify debug notifications [flags]
-c, --config string path to config file (default $HOME/.osem_notify.yml)
-d, --debug enable verbose logging
-l, --logformat string log format, can be plain or json (default "plain")
-n, --notify if set, will send out notifications.
Otherwise results are printed to stdout only.
--no-cache send all notifications, ignoring results from previous runs. also don't update the cache.
-n, --notify string If set, will send out notifications for the specified type of check result,
otherwise results are printed to stdout only.
Allowed values are "all", "error", "ok".
You might want to run 'osem_notify debug notifications' first to verify everything works.
Notifications for failing checks are sent only once, and then cached until the issue got
resolved, unless --no-cache is set.
To clear the cache, run 'osem_notify debug cache --clear'.
```
### SEE ALSO
* [osem_notify debug](osem_notify_debug.md) - Run some debugging checks on osem_notify itself
###### Auto generated by spf13/cobra on 26-Jun-2018
###### Auto generated by spf13/cobra on 10-Feb-2019

@ -23,14 +23,20 @@ osem_notify version [flags]
-c, --config string path to config file (default $HOME/.osem_notify.yml)
-d, --debug enable verbose logging
-l, --logformat string log format, can be plain or json (default "plain")
-n, --notify if set, will send out notifications.
Otherwise results are printed to stdout only.
--no-cache send all notifications, ignoring results from previous runs. also don't update the cache.
-n, --notify string If set, will send out notifications for the specified type of check result,
otherwise results are printed to stdout only.
Allowed values are "all", "error", "ok".
You might want to run 'osem_notify debug notifications' first to verify everything works.
Notifications for failing checks are sent only once, and then cached until the issue got
resolved, unless --no-cache is set.
To clear the cache, run 'osem_notify debug cache --clear'.
```
### SEE ALSO
* [osem_notify](osem_notify.md) - Root command displaying help
###### Auto generated by spf13/cobra on 26-Jun-2018
###### Auto generated by spf13/cobra on 10-Feb-2019

@ -10,7 +10,7 @@ Watch boxes for events at an interval
```
-h, --help help for watch
-i, --interval int interval to run checks in minutes (default 15)
-i, --interval int interval to run checks in minutes (default 30)
```
### Options inherited from parent commands
@ -20,15 +20,22 @@ Watch boxes for events at an interval
-c, --config string path to config file (default $HOME/.osem_notify.yml)
-d, --debug enable verbose logging
-l, --logformat string log format, can be plain or json (default "plain")
-n, --notify if set, will send out notifications.
Otherwise results are printed to stdout only.
--no-cache send all notifications, ignoring results from previous runs. also don't update the cache.
-n, --notify string If set, will send out notifications for the specified type of check result,
otherwise results are printed to stdout only.
Allowed values are "all", "error", "ok".
You might want to run 'osem_notify debug notifications' first to verify everything works.
Notifications for failing checks are sent only once, and then cached until the issue got
resolved, unless --no-cache is set.
To clear the cache, run 'osem_notify debug cache --clear'.
```
### SEE ALSO
* [osem_notify](osem_notify.md) - Root command displaying help
* [osem_notify watch all](osem_notify_watch_all.md) - watch all boxes registered on the map
* [osem_notify watch boxes](osem_notify_watch_boxes.md) - watch a list of box IDs for events
###### Auto generated by spf13/cobra on 26-Jun-2018
###### Auto generated by spf13/cobra on 10-Feb-2019

@ -0,0 +1,43 @@
## osem_notify watch all
watch all boxes registered on the map
### Synopsis
watch all boxes registered on the map
```
osem_notify watch all [flags]
```
### Options
```
-h, --help help for all
```
### Options inherited from parent commands
```
-a, --api string openSenseMap API to query against (default "https://api.opensensemap.org")
-c, --config string path to config file (default $HOME/.osem_notify.yml)
-d, --debug enable verbose logging
-i, --interval int interval to run checks in minutes (default 30)
-l, --logformat string log format, can be plain or json (default "plain")
--no-cache send all notifications, ignoring results from previous runs. also don't update the cache.
-n, --notify string If set, will send out notifications for the specified type of check result,
otherwise results are printed to stdout only.
Allowed values are "all", "error", "ok".
You might want to run 'osem_notify debug notifications' first to verify everything works.
Notifications for failing checks are sent only once, and then cached until the issue got
resolved, unless --no-cache is set.
To clear the cache, run 'osem_notify debug cache --clear'.
```
### SEE ALSO
* [osem_notify watch](osem_notify_watch.md) - Watch boxes for events at an interval
###### Auto generated by spf13/cobra on 10-Feb-2019

@ -22,16 +22,22 @@ osem_notify watch boxes <boxId> [...<boxIds>] [flags]
-a, --api string openSenseMap API to query against (default "https://api.opensensemap.org")
-c, --config string path to config file (default $HOME/.osem_notify.yml)
-d, --debug enable verbose logging
-i, --interval int interval to run checks in minutes (default 15)
-i, --interval int interval to run checks in minutes (default 30)
-l, --logformat string log format, can be plain or json (default "plain")
-n, --notify if set, will send out notifications.
Otherwise results are printed to stdout only.
--no-cache send all notifications, ignoring results from previous runs. also don't update the cache.
-n, --notify string If set, will send out notifications for the specified type of check result,
otherwise results are printed to stdout only.
Allowed values are "all", "error", "ok".
You might want to run 'osem_notify debug notifications' first to verify everything works.
Notifications for failing checks are sent only once, and then cached until the issue got
resolved, unless --no-cache is set.
To clear the cache, run 'osem_notify debug cache --clear'.
```
### SEE ALSO
* [osem_notify watch](osem_notify_watch.md) - Watch boxes for events at an interval
###### Auto generated by spf13/cobra on 26-Jun-2018
###### Auto generated by spf13/cobra on 10-Feb-2019

@ -3,6 +3,8 @@ package utils
import (
"os"
"path"
"strconv"
"strings"
log "github.com/sirupsen/logrus"
"github.com/spf13/viper"
@ -70,3 +72,7 @@ func PrintConfig() {
func printKV(key, val interface{}) {
log.Debugf("%20s: %v", key, val)
}
func ParseFloat(val string) (float64, error) {
return strconv.ParseFloat(strings.TrimSpace(val), 64)
}

Loading…
Cancel
Save