restructuring core package

- files are organized by functionality, not by class
- structs / interfaces are defined in the file that first outputs them

(not shure if i'll keep it,
i'm experimenting with go's namespaces,
bear with me)
develop
noerw 6 years ago
parent dfbbe9d58f
commit bf853e0ff4

@ -1,186 +0,0 @@
package core
import (
"crypto/sha256"
"encoding/hex"
"fmt"
log "github.com/sirupsen/logrus"
"github.com/spf13/viper"
)
type CheckResult struct {
Status string
Event string
Target string
TargetName string
Value string
Threshold string
}
func (r CheckResult) EventID() string {
s := fmt.Sprintf("%s%s%s", r.Event, r.Target, r.Threshold)
hasher := sha256.New()
hasher.Write([]byte(s))
return hex.EncodeToString(hasher.Sum(nil))
}
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)
} else {
return fmt.Sprintf("%s: "+checkTypes[r.Event].description+"\n", r.Status, r.TargetName, r.Target, r.Value)
}
}
type BoxCheckResults map[*Box][]CheckResult
func (results BoxCheckResults) Size() int {
size := 0
for _, boxResults := range results {
size += len(boxResults)
}
return size
}
func (results BoxCheckResults) Log() {
for box, boxResults := range results {
boxLog := log.WithFields(log.Fields{
"boxId": box.Id,
})
countErr := 0
for _, r := range boxResults {
resultLog := boxLog.WithFields(log.Fields{
"status": r.Status,
"event": r.Event,
"value": r.Value,
"target": r.Target,
})
if r.Status == CheckOk {
resultLog.Debugf("%s: %s", box.Name, r)
} else {
resultLog.Warnf("%s: %s", box.Name, r)
countErr++
}
}
if countErr == 0 {
boxLog.Infof("%s: all is fine!", box.Name)
}
}
}
func (results BoxCheckResults) SendNotifications() error {
// FIXME: don't return on errors, process all boxes first!
results = results.FilterChangedFromCache(false)
n := results.Size()
if n == 0 {
log.Info("No notifications due.")
return nil
} else {
log.Infof("Notifying for %v checks turned bad in total...", results.Size())
}
for box, resultsDue := range results {
if len(resultsDue) == 0 {
continue
}
transport := box.NotifyConf.Notifications.Transport
notifyLog := log.WithFields(log.Fields{
"boxId": box.Id,
"transport": transport,
})
notifier, err2 := box.GetNotifier()
if err2 != nil {
notifyLog.Error(err2)
return err2
}
notification := notifier.ComposeNotification(box, resultsDue)
err3 := notifier.Submit(notification)
if err3 != nil {
notifyLog.Error(err3)
return err3
}
notifyLog.Infof("Sent notification for %s via %s with %v new issues", box.Name, transport, len(resultsDue))
}
return nil
}
func (results BoxCheckResults) FilterChangedFromCache(keepOk bool) BoxCheckResults {
remaining := BoxCheckResults{}
for box, boxResults := range results {
// get results from cache. they are indexed by an event ID per boxId
// filter, so that only changed result.Status remain
remaining[box] = []CheckResult{}
for _, result := range boxResults {
cached := viper.GetStringMap(fmt.Sprintf("watchcache.%s.%s", box.Id, result.EventID()))
if result.Status != cached["laststatus"] {
if result.Status != CheckOk || keepOk {
remaining[box] = append(remaining[box], result)
}
}
}
// TODO: reminder functionality: extract additional results with Status ERR
// from cache with time.Since(lastNotifyDate) > remindAfter.
// would require to serialize the full result..
}
// upate cache, setting lastNotifyDate to Now()
for box, boxResults := range results {
for _, result := range boxResults {
// FIXME: somehow this is not persisted?
key := fmt.Sprintf("watchcache.%s.%s", box.Id, result.EventID())
viper.Set(key+".laststatus", result.Status)
}
}
return remaining
}
func CheckBoxes(boxIds []string, defaultConf *NotifyConfig) (BoxCheckResults, error) {
log.Debug("Checking notifications for ", len(boxIds), " box(es)")
results := BoxCheckResults{}
for _, boxId := range boxIds {
box, res, err := checkBox(boxId, defaultConf)
if err != nil {
return nil, err
}
results[box] = res
}
return results, nil
}
func checkBox(boxId string, defaultConf *NotifyConfig) (*Box, []CheckResult, error) {
boxLogger := log.WithFields(log.Fields{"boxId": boxId})
boxLogger.Info("checking box for events")
osem := NewOsemClient(viper.GetString("api"))
// get box data
box, err := osem.GetBox(boxId)
if err != nil {
boxLogger.Error(err)
return nil, nil, err
}
// if box has no notify config, we use the defaultConf
if box.NotifyConf == nil {
box.NotifyConf = defaultConf
}
// run checks
results, err2 := box.RunChecks()
if err2 != nil {
boxLogger.Error("could not run checks on box: ", err2)
return box, results, err2
}
return box, results, nil
}

@ -1,34 +0,0 @@
package core
import (
"errors"
"net/http"
"github.com/dghubble/sling"
)
type OsemError struct {
Code int `json:"code"`
Message string `json:"message"`
}
type OsemClient struct {
sling *sling.Sling
}
func NewOsemClient(endpoint string) *OsemClient {
return &OsemClient{
sling: sling.New().Client(&http.Client{}).Base(endpoint),
}
}
func (client *OsemClient) GetBox(boxId string) (*Box, error) {
box := &Box{}
fail := &OsemError{}
client.sling.New().Path("boxes/").Path(boxId).Receive(box, fail)
if fail.Message != "" {
return box, errors.New("could not fetch box: " + fail.Message)
}
return box, nil
}

@ -0,0 +1,87 @@
package core
import (
log "github.com/sirupsen/logrus"
"github.com/spf13/viper"
)
type BoxCheckResults map[*Box][]CheckResult
func (results BoxCheckResults) Size() int {
size := 0
for _, boxResults := range results {
size += len(boxResults)
}
return size
}
func (results BoxCheckResults) Log() {
for box, boxResults := range results {
boxLog := log.WithFields(log.Fields{
"boxId": box.Id,
})
countErr := 0
for _, r := range boxResults {
resultLog := boxLog.WithFields(log.Fields{
"status": r.Status,
"event": r.Event,
"value": r.Value,
"target": r.Target,
})
if r.Status == CheckOk {
resultLog.Debugf("%s: %s", box.Name, r)
} else {
resultLog.Warnf("%s: %s", box.Name, r)
countErr++
}
}
if countErr == 0 {
boxLog.Infof("%s: all is fine!", box.Name)
}
}
}
func CheckBoxes(boxIds []string, defaultConf *NotifyConfig) (BoxCheckResults, error) {
log.Debug("Checking notifications for ", len(boxIds), " box(es)")
results := BoxCheckResults{}
for _, boxId := range boxIds {
// TODO: check boxes in parallel, capped at 5 at once
box, res, err := checkBox(boxId, defaultConf)
if err != nil {
return nil, err
}
results[box] = res
}
return results, nil
}
func checkBox(boxId string, defaultConf *NotifyConfig) (*Box, []CheckResult, error) {
boxLogger := log.WithFields(log.Fields{"boxId": boxId})
boxLogger.Info("checking box for events")
osem := NewOsemClient(viper.GetString("api"))
// get box data
box, err := osem.GetBox(boxId)
if err != nil {
boxLogger.Error(err)
return nil, nil, err
}
// if box has no notify config, we use the defaultConf
if box.NotifyConf == nil {
box.NotifyConf = defaultConf
}
// run checks
results, err2 := box.RunChecks()
if err2 != nil {
boxLogger.Error("could not run checks on box: ", err2)
return box, results, err2
}
return box, results, nil
}

@ -1,6 +1,8 @@
package core
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"strconv"
"time"
@ -37,35 +39,28 @@ var faultyVals = map[FaultyValue]bool{
FaultyValue{sensor: "SDS 011", val: 0.0}: true,
}
type NotifyEvent struct {
Type string `json:"type"`
Target string `json:"target"`
Threshold string `json:"threshold"`
type CheckResult struct {
Status string
Event string
Target string
TargetName string
Value string
Threshold string
}
type TransportConfig struct {
Transport string `json:"transport"`
Options interface{} `json:"options"`
func (r CheckResult) EventID() string {
s := fmt.Sprintf("%s%s%s", r.Event, r.Target, r.Threshold)
hasher := sha256.New()
hasher.Write([]byte(s))
return hex.EncodeToString(hasher.Sum(nil))
}
type NotifyConfig struct {
Notifications TransportConfig `json:"notifications"`
Events []NotifyEvent `json:"events"`
}
type Box struct {
Id string `json:"_id"`
Name string `json:"name"`
Sensors []struct {
Id string `json:"_id"`
Phenomenon string `json:"title"`
Type string `json:"sensorType"`
LastMeasurement *struct {
Value string `json:"value"`
Date time.Time `json:"createdAt"`
} `json:"lastMeasurement"`
} `json:"sensors"`
NotifyConf *NotifyConfig `json:"healthcheck"`
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)
} else {
return fmt.Sprintf("%s: "+checkTypes[r.Event].description+"\n", r.Status, r.TargetName, r.Target, r.Value)
}
}
func (box Box) RunChecks() ([]CheckResult, error) {
@ -145,17 +140,3 @@ func (box Box) RunChecks() ([]CheckResult, error) {
return results, nil
}
func (box Box) GetNotifier() (AbstractNotifier, error) {
transport := box.NotifyConf.Notifications.Transport
if transport == "" {
return nil, fmt.Errorf("No notification transport provided")
}
notifier := notifiers[transport]
if notifier == nil {
return nil, fmt.Errorf("%s is not a supported notification transport", transport)
}
return notifier.New(box.NotifyConf.Notifications.Options)
}

@ -0,0 +1,84 @@
package core
import (
"errors"
"fmt"
"net/smtp"
"strings"
"time"
"github.com/spf13/viper"
)
// box config required for the EmailNotifier
type EmailNotifier struct {
Recipients []string
}
func (n EmailNotifier) New(config interface{}) (AbstractNotifier, error) {
// 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)
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{})
if ok {
asserted3, ok := asserted2["recipients"].([]interface{})
if ok {
asserted = EmailNotifier{Recipients: []string{}}
for _, rec := range asserted3 {
asserted.Recipients = append(asserted.Recipients, rec.(string))
}
}
}
if asserted.Recipients == nil {
return nil, errors.New("Invalid EmailNotifier options")
}
}
return EmailNotifier{
Recipients: asserted.Recipients,
}, 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 {
auth := smtp.PlainAuth(
"",
viper.GetString("email.user"),
viper.GetString("email.pass"),
viper.GetString("email.host"),
)
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)
// Connect to the server, authenticate, set the sender and recipient,
// and send the email all in one step.
err := smtp.SendMail(
fmt.Sprintf("%s:%s", viper.GetString("email.host"), viper.GetString("email.port")),
auth,
from,
n.Recipients,
[]byte(body),
)
return err
}

@ -1,12 +1,9 @@
package core
import (
"errors"
"fmt"
"net/smtp"
"strings"
"time"
log "github.com/sirupsen/logrus"
"github.com/spf13/viper"
)
@ -25,75 +22,92 @@ type Notification struct {
subject string
}
// box config required for the EmailNotifier
type EmailNotifier struct {
Recipients []string
//////
func (box Box) GetNotifier() (AbstractNotifier, error) {
transport := box.NotifyConf.Notifications.Transport
if transport == "" {
return nil, fmt.Errorf("No notification transport provided")
}
notifier := notifiers[transport]
if notifier == nil {
return nil, fmt.Errorf("%s is not a supported notification transport", transport)
}
return notifier.New(box.NotifyConf.Notifications.Options)
}
func (n EmailNotifier) New(config interface{}) (AbstractNotifier, error) {
// 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)
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{})
if ok {
asserted3, ok := asserted2["recipients"].([]interface{})
if ok {
asserted = EmailNotifier{Recipients: []string{}}
for _, rec := range asserted3 {
asserted.Recipients = append(asserted.Recipients, rec.(string))
}
}
func (results BoxCheckResults) SendNotifications() error {
// FIXME: don't return on errors, process all boxes first!
// FIXME: only update cache when notifications sent successfully
results = results.FilterChangedFromCache(false)
n := results.Size()
if n == 0 {
log.Info("No notifications due.")
return nil
} else {
log.Infof("Notifying for %v checks turned bad in total...", results.Size())
}
for box, resultsDue := range results {
if len(resultsDue) == 0 {
continue
}
if asserted.Recipients == nil {
return nil, errors.New("Invalid EmailNotifier options")
transport := box.NotifyConf.Notifications.Transport
notifyLog := log.WithFields(log.Fields{
"boxId": box.Id,
"transport": transport,
})
notifier, err2 := box.GetNotifier()
if err2 != nil {
notifyLog.Error(err2)
return err2
}
notification := notifier.ComposeNotification(box, resultsDue)
err3 := notifier.Submit(notification)
if err3 != nil {
notifyLog.Error(err3)
return err3
}
notifyLog.Infof("Sent notification for %s via %s with %v new issues", box.Name, transport, len(resultsDue))
}
return EmailNotifier{
Recipients: asserted.Recipients,
}, nil
return nil
}
func (n EmailNotifier) ComposeNotification(box *Box, checks []CheckResult) Notification {
resultTexts := []string{}
for _, check := range checks {
resultTexts = append(resultTexts, check.String())
func (results BoxCheckResults) FilterChangedFromCache(keepOk bool) BoxCheckResults {
remaining := BoxCheckResults{}
for box, boxResults := range results {
// get results from cache. they are indexed by an event ID per boxId
// filter, so that only changed result.Status remain
remaining[box] = []CheckResult{}
for _, result := range boxResults {
cached := viper.GetStringMap(fmt.Sprintf("watchcache.%s.%s", box.Id, result.EventID()))
if result.Status != cached["laststatus"] {
if result.Status != CheckOk || keepOk {
remaining[box] = append(remaining[box], result)
}
}
}
// TODO: reminder functionality: extract additional results with Status ERR
// from cache with time.Since(lastNotifyDate) > remindAfter.
// would require to serialize the full result..
}
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),
// upate cache, setting lastNotifyDate to Now()
for box, boxResults := range results {
for _, result := range boxResults {
// FIXME: somehow this is not persisted?
key := fmt.Sprintf("watchcache.%s.%s", box.Id, result.EventID())
viper.Set(key+".laststatus", result.Status)
}
}
}
func (n EmailNotifier) Submit(notification Notification) error {
auth := smtp.PlainAuth(
"",
viper.GetString("email.user"),
viper.GetString("email.pass"),
viper.GetString("email.host"),
)
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)
// Connect to the server, authenticate, set the sender and recipient,
// and send the email all in one step.
err := smtp.SendMail(
fmt.Sprintf("%s:%s", viper.GetString("email.host"), viper.GetString("email.port")),
auth,
from,
n.Recipients,
[]byte(body),
)
return err
return remaining
}

@ -0,0 +1,65 @@
package core
import (
"errors"
"net/http"
"time"
"github.com/dghubble/sling"
)
type OsemError struct {
Code int `json:"code"`
Message string `json:"message"`
}
type OsemClient struct {
sling *sling.Sling
}
func NewOsemClient(endpoint string) *OsemClient {
return &OsemClient{
sling: sling.New().Client(&http.Client{}).Base(endpoint),
}
}
func (client *OsemClient) GetBox(boxId string) (*Box, error) {
box := &Box{}
fail := &OsemError{}
client.sling.New().Path("boxes/").Path(boxId).Receive(box, fail)
if fail.Message != "" {
return box, errors.New("could not fetch box: " + fail.Message)
}
return box, nil
}
type NotifyEvent struct {
Type string `json:"type"`
Target string `json:"target"`
Threshold string `json:"threshold"`
}
type TransportConfig struct {
Transport string `json:"transport"`
Options interface{} `json:"options"`
}
type NotifyConfig struct {
Notifications TransportConfig `json:"notifications"`
Events []NotifyEvent `json:"events"`
}
type Box struct {
Id string `json:"_id"`
Name string `json:"name"`
Sensors []struct {
Id string `json:"_id"`
Phenomenon string `json:"title"`
Type string `json:"sensorType"`
LastMeasurement *struct {
Value string `json:"value"`
Date time.Time `json:"createdAt"`
} `json:"lastMeasurement"`
} `json:"sensors"`
NotifyConf *NotifyConfig `json:"healthcheck"`
}
Loading…
Cancel
Save