diff --git a/cmd/logmania/main.go b/cmd/logmania/main.go index 20fbd80..9844c18 100644 --- a/cmd/logmania/main.go +++ b/cmd/logmania/main.go @@ -59,6 +59,8 @@ func main() { } }() + go notifyState.Alert(config.Notify.AlertCheck.Duration, log.Save) + log.Info("starting logmania") receiver = allReceiver.Init(&config.Receive, logChannel) diff --git a/lib/config.go b/lib/config.go index b8351b3..721db3d 100644 --- a/lib/config.go +++ b/lib/config.go @@ -16,8 +16,9 @@ type Config struct { } type NotifyConfig struct { - StateFile string `toml:"state_file"` - XMPP struct { + StateFile string `toml:"state_file"` + AlertCheck Duration `toml:"alert_check"` + XMPP struct { Host string `toml:"host"` Username string `toml:"username"` Password string `toml:"password"` diff --git a/lib/duration.go b/lib/duration.go new file mode 100644 index 0000000..3fadaa4 --- /dev/null +++ b/lib/duration.go @@ -0,0 +1,56 @@ +package lib + +import ( + "fmt" + "strconv" + "time" +) + +// Duration is a TOML datatype +// A duration string is a possibly signed sequence of +// decimal numbers and a unit suffix, +// such as "300s", "1.5h" or "5d". +// Valid time units are "s", "m", "h", "d", "w". +type Duration struct { + time.Duration +} + +// UnmarshalTOML parses a duration string. +func (d *Duration) UnmarshalTOML(dataInterface interface{}) error { + var data string + switch dataInterface.(type) { + case string: + data = dataInterface.(string) + default: + return fmt.Errorf("invalid duration: \"%s\"", dataInterface) + } + // " + int + unit + " + if len(data) < 2 { + return fmt.Errorf("invalid duration: \"%s\"", data) + } + + unit := data[len(data)-1] + value, err := strconv.Atoi(string(data[:len(data)-1])) + if err != nil { + return fmt.Errorf("unable to parse duration %s: %s", data, err) + } + + switch unit { + case 's': + d.Duration = time.Duration(value) * time.Second + case 'm': + d.Duration = time.Duration(value) * time.Minute + case 'h': + d.Duration = time.Duration(value) * time.Hour + case 'd': + d.Duration = time.Duration(value) * time.Hour * 24 + case 'w': + d.Duration = time.Duration(value) * time.Hour * 24 * 7 + case 'y': + d.Duration = time.Duration(value) * time.Hour * 24 * 365 + default: + return fmt.Errorf("invalid duration unit: %s", string(unit)) + } + + return nil +} diff --git a/lib/duration_test.go b/lib/duration_test.go new file mode 100644 index 0000000..8a90cfd --- /dev/null +++ b/lib/duration_test.go @@ -0,0 +1,47 @@ +package lib + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestDuration(t *testing.T) { + assert := assert.New(t) + + var tests = []struct { + input string + err string + duration time.Duration + }{ + {"", "invalid duration: \"\"", 0}, + {"1x", "invalid duration unit: x", 0}, + {"1s", "", time.Second}, + {"73s", "", time.Second * 73}, + {"1m", "", time.Minute}, + {"73m", "", time.Minute * 73}, + {"1h", "", time.Hour}, + {"43h", "", time.Hour * 43}, + {"1d", "", time.Hour * 24}, + {"8d", "", time.Hour * 24 * 8}, + {"1w", "", time.Hour * 24 * 7}, + {"52w", "", time.Hour * 24 * 7 * 52}, + {"1y", "", time.Hour * 24 * 365}, + {"3y", "", time.Hour * 24 * 365 * 3}, + } + + for _, test := range tests { + + d := Duration{} + err := d.UnmarshalTOML(test.input) + duration := d.Duration + + if test.err == "" { + assert.NoError(err) + assert.Equal(test.duration, duration) + } else { + assert.EqualError(err, test.err) + } + } +} diff --git a/notify/config/config.go b/notify/config/config.go index 3294bef..363d40a 100644 --- a/notify/config/config.go +++ b/notify/config/config.go @@ -10,18 +10,22 @@ import ( "github.com/genofire/logmania/log" ) +const AlertMsg = "alert service from logmania, device did not send new message for a while" + type NotifyState struct { Hostname map[string]string `json:"hostname"` HostTo map[string]map[string]bool `json:"host_to"` MaxPrioIn map[string]log.LogLevel `json:"maxLevel"` RegexIn map[string]map[string]*regexp.Regexp `json:"regexIn"` Lastseen map[string]time.Time `json:"lastseen,omitempty"` - LastseenNotify map[string]time.Time `json:"lastseen_notify,omitempty"` + LastseenNotify map[string]time.Time `json:"-"` } func (state *NotifyState) SendTo(e *log.Entry) []string { if to, ok := state.HostTo[e.Hostname]; ok { - state.Lastseen[e.Hostname] = time.Now() + if e.Text != AlertMsg && e.Hostname != "" { + state.Lastseen[e.Hostname] = time.Now() + } var toList []string for toEntry, _ := range to { if lvl := state.MaxPrioIn[toEntry]; e.Level < lvl { @@ -108,6 +112,26 @@ func (state *NotifyState) Saver(path string) { } } +func (state *NotifyState) Alert(expired time.Duration, send func(e *log.Entry)) { + c := time.Tick(time.Minute) + + for range c { + now := time.Now() + for host, time := range state.Lastseen { + if time.Before(now.Add(expired * -2)) { + if timeNotify, ok := state.LastseenNotify[host]; !ok || !time.Before(timeNotify) { + state.LastseenNotify[host] = now + send(&log.Entry{ + Hostname: host, + Level: log.ErrorLevel, + Text: AlertMsg, + }) + } + } + } + } +} + // SaveJSON to path func (state *NotifyState) SaveJSON(outputFile string) { tmpFile := outputFile + ".tmp"