diff --git a/build/validate.sh b/build/validate.sh
index 035df237f3..cd7404bea4 100755
--- a/build/validate.sh
+++ b/build/validate.sh
@@ -61,7 +61,7 @@ if [ "$GOGENERATEDIFF" != '' ]; then
fi
echo -e "\nRunning go test bosun.org/..."
-go test bosun.org/...
+go test -v bosun.org/...
GOTESTRESULT=$?
if [ "$GOTESTRESULT" != 0 ]; then
BUILDMSG="${BUILDMSG}tests fail."
diff --git a/cmd/bosun/conf/conf.go b/cmd/bosun/conf/conf.go
index 4165f2ed96..193f53c48e 100644
--- a/cmd/bosun/conf/conf.go
+++ b/cmd/bosun/conf/conf.go
@@ -244,7 +244,7 @@ type Alert struct {
UnjoinedOK bool `json:",omitempty"`
Log bool
RunEvery int
- returnType eparse.FuncType
+ returnType models.FuncType
template string
squelch []string
@@ -934,7 +934,7 @@ func (c *Conf) loadAlert(s *parse.SectionNode) {
c.errorf("neither crit or warn specified")
}
var tags eparse.Tags
- var ret eparse.FuncType
+ var ret models.FuncType
if a.Crit != nil {
ctags, err := a.Crit.Root.Tags()
if err != nil {
@@ -1188,7 +1188,7 @@ func (c *Conf) NewExpr(s string) *expr.Expr {
c.error(err)
}
switch exp.Root.Return() {
- case eparse.TypeNumberSet, eparse.TypeScalar:
+ case models.TypeNumberSet, models.TypeScalar:
break
default:
c.errorf("expression must return a number")
@@ -1306,7 +1306,7 @@ func (c *Conf) Funcs() map[string]eparse.Func {
if err != nil {
return nil, err
}
- if a.returnType != eparse.TypeNumberSet {
+ if a.returnType != models.TypeNumberSet {
return nil, fmt.Errorf("alert requires a number-returning expression (got %v)", a.returnType)
}
return e.Root.Tags()
@@ -1314,20 +1314,20 @@ func (c *Conf) Funcs() map[string]eparse.Func {
funcs := map[string]eparse.Func{
"alert": {
- Args: []eparse.FuncType{eparse.TypeString, eparse.TypeString},
- Return: eparse.TypeNumberSet,
+ Args: []models.FuncType{models.TypeString, models.TypeString},
+ Return: models.TypeNumberSet,
Tags: tagAlert,
F: c.alert,
},
"lookup": {
- Args: []eparse.FuncType{eparse.TypeString, eparse.TypeString},
- Return: eparse.TypeNumberSet,
+ Args: []models.FuncType{models.TypeString, models.TypeString},
+ Return: models.TypeNumberSet,
Tags: lookupTags,
F: lookup,
},
"lookupSeries": {
- Args: []eparse.FuncType{eparse.TypeSeriesSet, eparse.TypeString, eparse.TypeString},
- Return: eparse.TypeNumberSet,
+ Args: []models.FuncType{models.TypeSeriesSet, models.TypeString, models.TypeString},
+ Return: models.TypeNumberSet,
Tags: lookupSeriesTags,
F: lookupSeries,
},
diff --git a/cmd/bosun/conf/notify.go b/cmd/bosun/conf/notify.go
index bbf4476faf..6fbd6ad65c 100644
--- a/cmd/bosun/conf/notify.go
+++ b/cmd/bosun/conf/notify.go
@@ -12,6 +12,7 @@ import (
"bosun.org/_third_party/github.com/jordan-wright/email"
"bosun.org/collect"
"bosun.org/metadata"
+ "bosun.org/models"
"bosun.org/slog"
"bosun.org/util"
)
@@ -25,7 +26,7 @@ func init() {
"The number of email notifications that Bosun failed to send.")
}
-func (n *Notification) Notify(subject, body string, emailsubject, emailbody []byte, c *Conf, ak string, attachments ...*Attachment) {
+func (n *Notification) Notify(subject, body string, emailsubject, emailbody []byte, c *Conf, ak string, attachments ...*models.Attachment) {
if len(n.Email) > 0 {
go n.DoEmail(emailsubject, emailbody, c, ak, attachments...)
}
@@ -93,13 +94,7 @@ func (n *Notification) DoGet(ak string) {
}
}
-type Attachment struct {
- Data []byte
- Filename string
- ContentType string
-}
-
-func (n *Notification) DoEmail(subject, body []byte, c *Conf, ak string, attachments ...*Attachment) {
+func (n *Notification) DoEmail(subject, body []byte, c *Conf, ak string, attachments ...*models.Attachment) {
e := email.NewEmail()
e.From = c.EmailFrom
for _, a := range n.Email {
diff --git a/cmd/bosun/database/database.go b/cmd/bosun/database/database.go
index 43702bb70b..1dba0d2cad 100644
--- a/cmd/bosun/database/database.go
+++ b/cmd/bosun/database/database.go
@@ -20,8 +20,8 @@ type DataAccess interface {
Metadata() MetadataDataAccess
Search() SearchDataAccess
Errors() ErrorDataAccess
+ State() StateDataAccess
Silence() SilenceDataAccess
- Incidents() IncidentDataAccess
}
type MetadataDataAccess interface {
diff --git a/cmd/bosun/database/incident_data.go b/cmd/bosun/database/incident_data.go
deleted file mode 100644
index a07f59bcd7..0000000000
--- a/cmd/bosun/database/incident_data.go
+++ /dev/null
@@ -1,128 +0,0 @@
-package database
-
-import (
- "encoding/json"
- "time"
-
- "bosun.org/_third_party/github.com/garyburd/redigo/redis"
- "bosun.org/collect"
- "bosun.org/models"
- "bosun.org/opentsdb"
-)
-
-/*
-
-incidents: hash of {id} -> json of incident
-maxIncidentId: counter. Increment to get next id.
-incidentsByStart: sorted set by start date
-
-*/
-
-type IncidentDataAccess interface {
- GetIncident(id uint64) (*models.Incident, error)
- CreateIncident(ak models.AlertKey, start time.Time) (*models.Incident, error)
- UpdateIncident(id uint64, i *models.Incident) error
-
- GetIncidentsStartingInRange(start, end time.Time) ([]*models.Incident, error)
-
- // should only be used by initial import
- SetMaxId(id uint64) error
-}
-
-func (d *dataAccess) Incidents() IncidentDataAccess {
- return d
-}
-
-func (d *dataAccess) GetIncident(id uint64) (*models.Incident, error) {
- defer collect.StartTimer("redis", opentsdb.TagSet{"op": "GetIncident"})()
- conn := d.GetConnection()
- defer conn.Close()
- raw, err := redis.Bytes(conn.Do("HGET", "incidents", id))
- if err != nil {
- return nil, err
- }
- incident := &models.Incident{}
- if err = json.Unmarshal(raw, incident); err != nil {
- return nil, err
- }
- return incident, nil
-}
-
-func (d *dataAccess) CreateIncident(ak models.AlertKey, start time.Time) (*models.Incident, error) {
- defer collect.StartTimer("redis", opentsdb.TagSet{"op": "CreateIncident"})()
- conn := d.GetConnection()
- defer conn.Close()
- id, err := redis.Int64(conn.Do("INCR", "maxIncidentId"))
- if err != nil {
- return nil, err
- }
- incident := &models.Incident{
- Id: uint64(id),
- Start: start,
- AlertKey: ak,
- }
- err = saveIncident(incident.Id, incident, conn)
- if err != nil {
- return nil, err
- }
- return incident, nil
-}
-
-func saveIncident(id uint64, i *models.Incident, conn redis.Conn) error {
- raw, err := json.Marshal(i)
- if err != nil {
- return err
- }
- if _, err = conn.Do("HSET", "incidents", id, raw); err != nil {
- return err
- }
- if _, err = conn.Do("ZADD", "incidentsByStart", i.Start.UTC().Unix(), id); err != nil {
- return err
- }
- return nil
-}
-
-func (d *dataAccess) GetIncidentsStartingInRange(start, end time.Time) ([]*models.Incident, error) {
- defer collect.StartTimer("redis", opentsdb.TagSet{"op": "GetIncidentsStartingInRange"})()
- conn := d.GetConnection()
- defer conn.Close()
-
- ids, err := redis.Ints(conn.Do("ZRANGEBYSCORE", "incidentsByStart", start.UTC().Unix(), end.UTC().Unix()))
- if err != nil {
- return nil, err
- }
- args := make([]interface{}, len(ids)+1)
- args[0] = "incidents"
- for i := range ids {
- args[i+1] = ids[i]
- }
- jsons, err := redis.Strings(conn.Do("HMGET", args...))
- if err != nil {
- return nil, err
- }
- incidents := make([]*models.Incident, len(jsons))
- for i := range jsons {
- inc := &models.Incident{}
- if err = json.Unmarshal([]byte(jsons[i]), inc); err != nil {
- return nil, err
- }
- incidents[i] = inc
- }
- return incidents, nil
-}
-
-func (d *dataAccess) UpdateIncident(id uint64, i *models.Incident) error {
- defer collect.StartTimer("redis", opentsdb.TagSet{"op": "UpdateIncident"})()
- conn := d.GetConnection()
- defer conn.Close()
- return saveIncident(id, i, conn)
-}
-
-func (d *dataAccess) SetMaxId(id uint64) error {
- defer collect.StartTimer("redis", opentsdb.TagSet{"op": "SetMaxId"})()
- conn := d.GetConnection()
- defer conn.Close()
-
- _, err := conn.Do("SET", "maxIncidentId", id)
- return err
-}
diff --git a/cmd/bosun/database/state_data.go b/cmd/bosun/database/state_data.go
new file mode 100644
index 0000000000..09ed4bac01
--- /dev/null
+++ b/cmd/bosun/database/state_data.go
@@ -0,0 +1,397 @@
+package database
+
+import (
+ "encoding/json"
+ "fmt"
+ "time"
+
+ "bosun.org/_third_party/github.com/garyburd/redigo/redis"
+ "bosun.org/collect"
+ "bosun.org/models"
+ "bosun.org/opentsdb"
+ "bosun.org/slog"
+)
+
+/*
+incidentById:{id} - json encoded state. Authoritative source.
+
+lastTouched:{alert} - ZSET of alert key to last touched time stamp
+unknown:{alert} - Set of unknown alert keys for alert
+unevel:{alert} - Set of unevaluated alert keys for alert
+
+openIncidents - Hash of open incident Ids. Alert Key -> incident id
+incidents:{ak} - List of incidents for alert key
+
+allIncidents - List of all incidents ever. Value is "incidentId:timestamp:ak"
+*/
+
+const (
+ statesOpenIncidentsKey = "openIncidents"
+)
+
+func statesLastTouchedKey(alert string) string {
+ return fmt.Sprintf("lastTouched:%s", alert)
+}
+func statesUnknownKey(alert string) string {
+ return fmt.Sprintf("unknown:%s", alert)
+}
+func statesUnevalKey(alert string) string {
+ return fmt.Sprintf("uneval:%s", alert)
+}
+func incidentStateKey(id int64) string {
+ return fmt.Sprintf("incidentById:%d", id)
+}
+func incidentsForAlertKeyKey(ak models.AlertKey) string {
+ return fmt.Sprintf("incidents:%s", ak)
+}
+
+type StateDataAccess interface {
+ TouchAlertKey(ak models.AlertKey, t time.Time) error
+ GetUntouchedSince(alert string, time int64) ([]models.AlertKey, error)
+
+ GetOpenIncident(ak models.AlertKey) (*models.IncidentState, error)
+ GetLatestIncident(ak models.AlertKey) (*models.IncidentState, error)
+ GetAllOpenIncidents() ([]*models.IncidentState, error)
+ GetIncidentState(incidentId int64) (*models.IncidentState, error)
+ GetAllIncidents(ak models.AlertKey) ([]*models.IncidentState, error)
+
+ UpdateIncidentState(s *models.IncidentState) error
+ ImportIncidentState(s *models.IncidentState) error
+
+ Forget(ak models.AlertKey) error
+ SetUnevaluated(ak models.AlertKey, uneval bool) error
+ GetUnknownAndUnevalAlertKeys(alert string) ([]models.AlertKey, []models.AlertKey, error)
+}
+
+func (d *dataAccess) State() StateDataAccess {
+ return d
+}
+
+func (d *dataAccess) TouchAlertKey(ak models.AlertKey, t time.Time) error {
+ defer collect.StartTimer("redis", opentsdb.TagSet{"op": "TouchAlertKey"})()
+ conn := d.GetConnection()
+ defer conn.Close()
+
+ _, err := conn.Do("ZADD", statesLastTouchedKey(ak.Name()), t.UTC().Unix(), string(ak))
+ return slog.Wrap(err)
+}
+
+func (d *dataAccess) GetUntouchedSince(alert string, time int64) ([]models.AlertKey, error) {
+ defer collect.StartTimer("redis", opentsdb.TagSet{"op": "GetUntouchedSince"})()
+ conn := d.GetConnection()
+ defer conn.Close()
+
+ results, err := redis.Strings(conn.Do("ZRANGEBYSCORE", statesLastTouchedKey(alert), "-inf", time))
+ if err != nil {
+ return nil, slog.Wrap(err)
+ }
+ aks := make([]models.AlertKey, len(results))
+ for i := range results {
+ aks[i] = models.AlertKey(results[i])
+ }
+ return aks, nil
+}
+
+func (d *dataAccess) GetOpenIncident(ak models.AlertKey) (*models.IncidentState, error) {
+ defer collect.StartTimer("redis", opentsdb.TagSet{"op": "GetOpenIncident"})()
+ conn := d.GetConnection()
+ defer conn.Close()
+
+ inc, err := d.getLatestIncident(ak, conn)
+ if err != nil {
+ return nil, slog.Wrap(err)
+ }
+ if inc == nil {
+ return nil, nil
+ }
+ if inc.Open {
+ return inc, nil
+ }
+ return nil, nil
+}
+
+func (d *dataAccess) getLatestIncident(ak models.AlertKey, conn redis.Conn) (*models.IncidentState, error) {
+ id, err := redis.Int64(conn.Do("LINDEX", incidentsForAlertKeyKey(ak), 0))
+ if err != nil {
+ if err == redis.ErrNil {
+ return nil, nil
+ }
+ return nil, slog.Wrap(err)
+ }
+ inc, err := d.getIncident(id, conn)
+ if err != nil {
+ return nil, slog.Wrap(err)
+ }
+ return inc, nil
+}
+
+func (d *dataAccess) GetLatestIncident(ak models.AlertKey) (*models.IncidentState, error) {
+ defer collect.StartTimer("redis", opentsdb.TagSet{"op": "GetLatestIncident"})()
+ conn := d.GetConnection()
+ defer conn.Close()
+
+ return d.getLatestIncident(ak, conn)
+}
+
+func (d *dataAccess) GetAllOpenIncidents() ([]*models.IncidentState, error) {
+ defer collect.StartTimer("redis", opentsdb.TagSet{"op": "GetAllOpenIncidents"})()
+ conn := d.GetConnection()
+ defer conn.Close()
+
+ // get open ids
+ ids, err := int64s(conn.Do("HVALS", statesOpenIncidentsKey))
+ if err != nil {
+ return nil, slog.Wrap(err)
+ }
+ return d.incidentMultiGet(conn, ids)
+}
+
+func (d *dataAccess) GetAllIncidents(ak models.AlertKey) ([]*models.IncidentState, error) {
+ defer collect.StartTimer("redis", opentsdb.TagSet{"op": "GetAllIncidents"})()
+ conn := d.GetConnection()
+ defer conn.Close()
+
+ ids, err := int64s(conn.Do("LRANGE", incidentsForAlertKeyKey(ak), 0, -1))
+ if err != nil {
+ return nil, slog.Wrap(err)
+ }
+ return d.incidentMultiGet(conn, ids)
+}
+
+func (d *dataAccess) incidentMultiGet(conn redis.Conn, ids []int64) ([]*models.IncidentState, error) {
+ if len(ids) == 0 {
+ return nil, nil
+ }
+ // get all incident json keys
+ args := make([]interface{}, 0, len(ids))
+ for _, id := range ids {
+ args = append(args, incidentStateKey(id))
+ }
+ jsons, err := redis.Strings(conn.Do("MGET", args...))
+ if err != nil {
+ return nil, slog.Wrap(err)
+ }
+ results := make([]*models.IncidentState, 0, len(jsons))
+ for _, j := range jsons {
+ state := &models.IncidentState{}
+ if err = json.Unmarshal([]byte(j), state); err != nil {
+ return nil, slog.Wrap(err)
+ }
+ results = append(results, state)
+ }
+ return results, nil
+}
+
+func (d *dataAccess) getIncident(incidentId int64, conn redis.Conn) (*models.IncidentState, error) {
+ b, err := redis.Bytes(conn.Do("GET", incidentStateKey(incidentId)))
+ if err != nil {
+ return nil, slog.Wrap(err)
+ }
+ state := &models.IncidentState{}
+ if err = json.Unmarshal(b, state); err != nil {
+ return nil, slog.Wrap(err)
+ }
+ return state, nil
+}
+
+func (d *dataAccess) GetIncidentState(incidentId int64) (*models.IncidentState, error) {
+ defer collect.StartTimer("redis", opentsdb.TagSet{"op": "GetIncident"})()
+ conn := d.GetConnection()
+ defer conn.Close()
+ return d.getIncident(incidentId, conn)
+}
+
+func (d *dataAccess) UpdateIncidentState(s *models.IncidentState) error {
+ return d.save(s, false)
+}
+
+func (d *dataAccess) ImportIncidentState(s *models.IncidentState) error {
+ return d.save(s, true)
+}
+
+func (d *dataAccess) save(s *models.IncidentState, isImport bool) error {
+ defer collect.StartTimer("redis", opentsdb.TagSet{"op": "UpdateIncident"})()
+ conn := d.GetConnection()
+ defer conn.Close()
+
+ isNew := false
+ //if id is still zero, assign new id.
+ if s.Id == 0 {
+ id, err := redis.Int64(conn.Do("INCR", "maxIncidentId"))
+ if err != nil {
+ return slog.Wrap(err)
+ }
+ s.Id = id
+ isNew = true
+ } else if isImport {
+ max, err := redis.Int64(conn.Do("GET", "maxIncidentId"))
+ if err != nil {
+ max = 0
+ }
+ if max < s.Id {
+ if _, err = conn.Do("SET", "maxIncidentId", s.Id); err != nil {
+ return slog.Wrap(err)
+ }
+ }
+ isNew = true
+ }
+ return d.transact(conn, func() error {
+ if isNew {
+ // add to list for alert key
+ if _, err := conn.Do("LPUSH", incidentsForAlertKeyKey(s.AlertKey), s.Id); err != nil {
+ return slog.Wrap(err)
+ }
+ dat := fmt.Sprintf("%d:%d:%s", s.Id, s.Start.UTC().Unix(), s.AlertKey)
+ if _, err := conn.Do("LPUSH", "allIncidents", dat); err != nil {
+ return slog.Wrap(err)
+ }
+ }
+
+ // store the incident json
+ data, err := json.Marshal(s)
+ if err != nil {
+ return slog.Wrap(err)
+ }
+ _, err = conn.Do("SET", incidentStateKey(s.Id), data)
+
+ addRem := func(b bool) string {
+ if b {
+ return "SADD"
+ }
+ return "SREM"
+ }
+ // appropriately add or remove it from the "open" set
+ if s.Open {
+ if _, err = conn.Do("HSET", statesOpenIncidentsKey, s.AlertKey, s.Id); err != nil {
+ return slog.Wrap(err)
+ }
+ } else {
+ if _, err = conn.Do("HDEL", statesOpenIncidentsKey, s.AlertKey); err != nil {
+ return slog.Wrap(err)
+ }
+ }
+
+ //appropriately add or remove from unknown and uneval sets
+ if _, err = conn.Do(addRem(s.CurrentStatus == models.StUnknown), statesUnknownKey(s.Alert), s.AlertKey); err != nil {
+ return slog.Wrap(err)
+ }
+ if _, err = conn.Do(addRem(s.Unevaluated), statesUnevalKey(s.Alert), s.AlertKey); err != nil {
+ return slog.Wrap(err)
+ }
+ return nil
+ })
+}
+
+func (d *dataAccess) SetUnevaluated(ak models.AlertKey, uneval bool) error {
+ defer collect.StartTimer("redis", opentsdb.TagSet{"op": "SetUnevaluated"})()
+ conn := d.GetConnection()
+ defer conn.Close()
+
+ op := "SREM"
+ if uneval {
+ op = "SADD"
+ }
+ _, err := conn.Do(op, statesUnevalKey(ak.Name()), ak)
+ return slog.Wrap(err)
+}
+
+// The nucular option. Delete all we know about this alert key
+func (d *dataAccess) Forget(ak models.AlertKey) error {
+ defer collect.StartTimer("redis", opentsdb.TagSet{"op": "Forget"})()
+ conn := d.GetConnection()
+ defer conn.Close()
+
+ alert := ak.Name()
+ return d.transact(conn, func() error {
+ // last touched.
+ if _, err := conn.Do("HDEL", statesLastTouchedKey(alert), ak); err != nil {
+ return slog.Wrap(err)
+ }
+ // unknown/uneval sets
+ if _, err := conn.Do("SREM", statesUnknownKey(alert), ak); err != nil {
+ return slog.Wrap(err)
+ }
+ if _, err := conn.Do("SREM", statesUnevalKey(alert), ak); err != nil {
+ return slog.Wrap(err)
+ }
+ //open set
+ if _, err := conn.Do("HDEL", statesOpenIncidentsKey, ak); err != nil {
+ return slog.Wrap(err)
+ }
+ //all incidents
+ ids, err := int64s(conn.Do("LRANGE", incidentsForAlertKeyKey(ak), 0, -1))
+ if err != nil {
+ return slog.Wrap(err)
+ }
+ if _, err = conn.Do("HDEL", statesOpenIncidentsKey, ak); err != nil {
+ return slog.Wrap(err)
+ }
+ for _, id := range ids {
+
+ if _, err = conn.Do("DEL", incidentStateKey(id)); err != nil {
+ return slog.Wrap(err)
+ }
+ }
+ if _, err := conn.Do(d.LCLEAR(), incidentsForAlertKeyKey(ak)); err != nil {
+ return slog.Wrap(err)
+ }
+ return nil
+ })
+}
+
+func (d *dataAccess) GetUnknownAndUnevalAlertKeys(alert string) ([]models.AlertKey, []models.AlertKey, error) {
+ defer collect.StartTimer("redis", opentsdb.TagSet{"op": "GetUnknownAndUnevalAlertKeys"})()
+ conn := d.GetConnection()
+ defer conn.Close()
+
+ unknownS, err := redis.Strings(conn.Do("SMEMBERS", statesUnknownKey(alert)))
+ if err != nil {
+ return nil, nil, slog.Wrap(err)
+ }
+ unknown := make([]models.AlertKey, len(unknownS))
+ for i, u := range unknownS {
+ unknown[i] = models.AlertKey(u)
+ }
+
+ unEvals, err := redis.Strings(conn.Do("SMEMBERS", statesUnevalKey(alert)))
+ if err != nil {
+ return nil, nil, slog.Wrap(err)
+ }
+ unevals := make([]models.AlertKey, len(unEvals))
+ for i, u := range unEvals {
+ unevals[i] = models.AlertKey(u)
+ }
+
+ return unknown, unevals, nil
+}
+
+func int64s(reply interface{}, err error) ([]int64, error) {
+ if err != nil {
+ return nil, slog.Wrap(err)
+ }
+ ints := []int64{}
+ values, err := redis.Values(reply, err)
+ if err != nil {
+ return ints, slog.Wrap(err)
+ }
+ if err := redis.ScanSlice(values, &ints); err != nil {
+ return ints, slog.Wrap(err)
+ }
+ return ints, nil
+}
+
+func (d *dataAccess) transact(conn redis.Conn, f func() error) error {
+ if !d.isRedis {
+ return f()
+ }
+ if _, err := conn.Do("MULTI"); err != nil {
+ return slog.Wrap(err)
+ }
+ if err := f(); err != nil {
+ return slog.Wrap(err)
+ }
+ if _, err := conn.Do("EXEC"); err != nil {
+ return slog.Wrap(err)
+ }
+ return nil
+}
diff --git a/cmd/bosun/database/test/database_test.go b/cmd/bosun/database/test/database_test.go
index bf0f6764dc..adebb3b5de 100644
--- a/cmd/bosun/database/test/database_test.go
+++ b/cmd/bosun/database/test/database_test.go
@@ -18,7 +18,7 @@ var testData database.DataAccess
func TestMain(m *testing.M) {
rand.Seed(time.Now().UnixNano())
var closeF func()
- testData, closeF = StartTestRedis()
+ testData, closeF = StartTestRedis(9993)
status := m.Run()
closeF()
os.Exit(status)
diff --git a/cmd/bosun/database/test/incidents_test.go b/cmd/bosun/database/test/incidents_test.go
deleted file mode 100644
index ce52efe597..0000000000
--- a/cmd/bosun/database/test/incidents_test.go
+++ /dev/null
@@ -1,65 +0,0 @@
-package dbtest
-
-import (
- "testing"
- "time"
-)
-
-func TestIncidents_RoundTrip(t *testing.T) {
- inc := testData.Incidents()
-
- i, err := inc.CreateIncident("foo{host=3}", time.Now().UTC())
- check(t, err)
-
- i2, err := inc.CreateIncident("foo{host=3}", time.Now().UTC())
- check(t, err)
-
- if i.Id >= i2.Id {
- t.Fatal("Expect ids to be ascending")
- }
-
- readBack, err := inc.GetIncident(i.Id)
- check(t, err)
- if readBack.AlertKey != i.AlertKey {
- t.Fatal("Alert key's don't match")
- }
-
- tm := time.Now().UTC().Add(42 * time.Hour)
- i.End = &tm
- check(t, inc.UpdateIncident(i.Id, i))
-
- readBack, err = inc.GetIncident(i.Id)
- check(t, err)
- if *readBack.End != tm {
- t.Fatal("End times don't match")
- }
-}
-
-func TestIncidentSearch(t *testing.T) {
- inc := testData.Incidents()
-
- startTime := time.Now().Add(-5000 * time.Hour).UTC()
-
- i, err := inc.CreateIncident("foo2{host=3}", startTime)
- check(t, err)
-
- i2, err := inc.CreateIncident("foo2{host=4}", startTime)
- check(t, err)
-
- _, err = inc.CreateIncident("foo2{host=4}", startTime.Add(500*time.Hour))
- check(t, err)
- _, err = inc.CreateIncident("foo2{host=4}", startTime.Add(-500*time.Hour))
- check(t, err)
- i3, err := inc.CreateIncident("BAR{host=4}", startTime)
- check(t, err)
-
- results, err := inc.GetIncidentsStartingInRange(startTime.Add(-1*time.Hour), startTime.Add(time.Hour))
- check(t, err)
-
- if len(results) != 3 {
- t.Fatal("Wrong number of results")
- }
- if results[0].Id != i.Id || results[1].Id != i2.Id || results[2].Id != i3.Id {
- t.Fatal("Ids don't match", results, i.Id, i2.Id, i3.Id)
- }
-}
diff --git a/cmd/bosun/database/test/testSetup.go b/cmd/bosun/database/test/testSetup.go
index 64635c92ec..5222d711d3 100644
--- a/cmd/bosun/database/test/testSetup.go
+++ b/cmd/bosun/database/test/testSetup.go
@@ -14,7 +14,7 @@ import (
var flagReddisHost = flag.String("redis", "", "redis server to test against")
var flagFlushRedis = flag.Bool("flush", false, "flush database before tests. DANGER!")
-func StartTestRedis() (database.DataAccess, func()) {
+func StartTestRedis(port int) (database.DataAccess, func()) {
flag.Parse()
// For redis tests we just point at an external server.
if *flagReddisHost != "" {
@@ -31,9 +31,9 @@ func StartTestRedis() (database.DataAccess, func()) {
return testData, func() {}
}
// To test ledis, start a local instance in a new tmp dir. We will attempt to delete it when we're done.
- addr := "127.0.0.1:9876"
- testPath := filepath.Join(os.TempDir(), "bosun_ledis_test", fmt.Sprint(time.Now().Unix()))
- log.Println(testPath)
+ addr := fmt.Sprintf("127.0.0.1:%d", port)
+ testPath := filepath.Join(os.TempDir(), "bosun_ledis_test", fmt.Sprintf("%d-%d", time.Now().UnixNano(), port))
+ log.Println("Test ledis at", testPath, addr)
stop, err := database.StartLedis(testPath, addr)
if err != nil {
log.Fatal(err)
diff --git a/cmd/bosun/expr/elastic.go b/cmd/bosun/expr/elastic.go
index 6eae3570ad..b845c62174 100644
--- a/cmd/bosun/expr/elastic.go
+++ b/cmd/bosun/expr/elastic.go
@@ -9,6 +9,7 @@ import (
"bosun.org/_third_party/github.com/MiniProfiler/go/miniprofiler"
elastic "bosun.org/_third_party/gopkg.in/olivere/elastic.v3"
"bosun.org/cmd/bosun/expr/parse"
+ "bosun.org/models"
"bosun.org/opentsdb"
)
@@ -29,85 +30,85 @@ func elasticTagQuery(args []parse.Node) (parse.Tags, error) {
var Elastic = map[string]parse.Func{
// Funcs for querying elastic
"escount": {
- Args: []parse.FuncType{parse.TypeESIndexer, parse.TypeString, parse.TypeESQuery, parse.TypeString, parse.TypeString, parse.TypeString},
- Return: parse.TypeSeriesSet,
+ Args: []models.FuncType{models.TypeESIndexer, models.TypeString, models.TypeESQuery, models.TypeString, models.TypeString, models.TypeString},
+ Return: models.TypeSeriesSet,
Tags: elasticTagQuery,
F: ESCount,
},
"esstat": {
- Args: []parse.FuncType{parse.TypeESIndexer, parse.TypeString, parse.TypeESQuery, parse.TypeString, parse.TypeString, parse.TypeString, parse.TypeString, parse.TypeString},
- Return: parse.TypeSeriesSet,
+ Args: []models.FuncType{models.TypeESIndexer, models.TypeString, models.TypeESQuery, models.TypeString, models.TypeString, models.TypeString, models.TypeString, models.TypeString},
+ Return: models.TypeSeriesSet,
Tags: elasticTagQuery,
F: ESStat,
},
// Funcs to create elastic index names (ESIndexer type)
"esindices": {
- Args: []parse.FuncType{parse.TypeString, parse.TypeString},
+ Args: []models.FuncType{models.TypeString, models.TypeString},
VArgs: true,
VArgsPos: 1,
- Return: parse.TypeESIndexer,
+ Return: models.TypeESIndexer,
F: ESIndicies,
},
"esdaily": {
- Args: []parse.FuncType{parse.TypeString, parse.TypeString, parse.TypeString},
+ Args: []models.FuncType{models.TypeString, models.TypeString, models.TypeString},
VArgs: true,
VArgsPos: 1,
- Return: parse.TypeESIndexer,
+ Return: models.TypeESIndexer,
F: ESDaily,
},
"esls": {
- Args: []parse.FuncType{parse.TypeString},
- Return: parse.TypeESIndexer,
+ Args: []models.FuncType{models.TypeString},
+ Return: models.TypeESIndexer,
F: ESLS,
},
// Funcs for generate elastic queries (ESQuery Type) to further filter results
"esall": {
- Args: []parse.FuncType{},
- Return: parse.TypeESQuery,
+ Args: []models.FuncType{},
+ Return: models.TypeESQuery,
F: ESAll,
},
"esregexp": {
- Args: []parse.FuncType{parse.TypeString, parse.TypeString},
- Return: parse.TypeESQuery,
+ Args: []models.FuncType{models.TypeString, models.TypeString},
+ Return: models.TypeESQuery,
F: ESRegexp,
},
"esquery": {
- Args: []parse.FuncType{parse.TypeString, parse.TypeString},
- Return: parse.TypeESQuery,
+ Args: []models.FuncType{models.TypeString, models.TypeString},
+ Return: models.TypeESQuery,
F: ESQueryString,
},
"esand": {
- Args: []parse.FuncType{parse.TypeESQuery},
+ Args: []models.FuncType{models.TypeESQuery},
VArgs: true,
- Return: parse.TypeESQuery,
+ Return: models.TypeESQuery,
F: ESAnd,
},
"esor": {
- Args: []parse.FuncType{parse.TypeESQuery},
+ Args: []models.FuncType{models.TypeESQuery},
VArgs: true,
- Return: parse.TypeESQuery,
+ Return: models.TypeESQuery,
F: ESOr,
},
"esgt": {
- Args: []parse.FuncType{parse.TypeString, parse.TypeScalar},
- Return: parse.TypeESQuery,
+ Args: []models.FuncType{models.TypeString, models.TypeScalar},
+ Return: models.TypeESQuery,
F: ESGT,
},
"esgte": {
- Args: []parse.FuncType{parse.TypeString, parse.TypeScalar},
- Return: parse.TypeESQuery,
+ Args: []models.FuncType{models.TypeString, models.TypeScalar},
+ Return: models.TypeESQuery,
F: ESGTE,
},
"eslt": {
- Args: []parse.FuncType{parse.TypeString, parse.TypeScalar},
- Return: parse.TypeESQuery,
+ Args: []models.FuncType{models.TypeString, models.TypeScalar},
+ Return: models.TypeESQuery,
F: ESLT,
},
"eslte": {
- Args: []parse.FuncType{parse.TypeString, parse.TypeScalar},
- Return: parse.TypeESQuery,
+ Args: []models.FuncType{models.TypeString, models.TypeScalar},
+ Return: models.TypeESQuery,
F: ESLTE,
},
}
diff --git a/cmd/bosun/expr/expr.go b/cmd/bosun/expr/expr.go
index f845ac84df..86e31c177c 100644
--- a/cmd/bosun/expr/expr.go
+++ b/cmd/bosun/expr/expr.go
@@ -140,11 +140,6 @@ func errRecover(errp *error) {
}
}
-type Value interface {
- Type() parse.FuncType
- Value() interface{}
-}
-
func marshalFloat(n float64) ([]byte, error) {
if math.IsNaN(n) {
return json.Marshal("NaN")
@@ -156,23 +151,28 @@ func marshalFloat(n float64) ([]byte, error) {
return json.Marshal(n)
}
+type Value interface {
+ Type() models.FuncType
+ Value() interface{}
+}
+
type Number float64
-func (n Number) Type() parse.FuncType { return parse.TypeNumberSet }
+func (n Number) Type() models.FuncType { return models.TypeNumberSet }
func (n Number) Value() interface{} { return n }
func (n Number) MarshalJSON() ([]byte, error) { return marshalFloat(float64(n)) }
type Scalar float64
-func (s Scalar) Type() parse.FuncType { return parse.TypeScalar }
+func (s Scalar) Type() models.FuncType { return models.TypeScalar }
func (s Scalar) Value() interface{} { return s }
func (s Scalar) MarshalJSON() ([]byte, error) { return marshalFloat(float64(s)) }
// Series is the standard form within bosun to represent timeseries data.
type Series map[time.Time]float64
-func (s Series) Type() parse.FuncType { return parse.TypeSeriesSet }
-func (s Series) Value() interface{} { return s }
+func (s Series) Type() models.FuncType { return models.TypeSeriesSet }
+func (s Series) Value() interface{} { return s }
func (s Series) MarshalJSON() ([]byte, error) {
r := make(map[string]interface{}, len(s))
@@ -186,8 +186,8 @@ type ESQuery struct {
Query elastic.Query
}
-func (e ESQuery) Type() parse.FuncType { return parse.TypeESQuery }
-func (e ESQuery) Value() interface{} { return e }
+func (e ESQuery) Type() models.FuncType { return models.TypeESQuery }
+func (e ESQuery) Value() interface{} { return e }
func (e ESQuery) MarshalJSON() ([]byte, error) {
source, err := e.Query.Source()
if err != nil {
@@ -201,8 +201,8 @@ type ESIndexer struct {
Generate func(startDuration, endDuration *time.Time) ([]string, error)
}
-func (e ESIndexer) Type() parse.FuncType { return parse.TypeESIndexer }
-func (e ESIndexer) Value() interface{} { return e }
+func (e ESIndexer) Type() models.FuncType { return models.TypeESIndexer }
+func (e ESIndexer) Value() interface{} { return e }
func (e ESIndexer) MarshalJSON() ([]byte, error) {
return json.Marshal("ESGenerator")
}
@@ -231,7 +231,7 @@ func NewSortedSeries(dps Series) SortableSeries {
}
type Result struct {
- Computations
+ models.Computations
Value
Group opentsdb.TagSet
}
@@ -289,22 +289,15 @@ func (r ResultSliceByGroup) Len() int { return len(r) }
func (r ResultSliceByGroup) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
func (r ResultSliceByGroup) Less(i, j int) bool { return r[i].Group.String() < r[j].Group.String() }
-type Computations []Computation
-
-type Computation struct {
- Text string
- Value interface{}
-}
-
func (e *State) AddComputation(r *Result, text string, value interface{}) {
if !e.enableComputations {
return
}
- r.Computations = append(r.Computations, Computation{opentsdb.ReplaceTags(text, r.Group), value})
+ r.Computations = append(r.Computations, models.Computation{Text: opentsdb.ReplaceTags(text, r.Group), Value: value})
}
type Union struct {
- Computations
+ models.Computations
A, B Value
Group opentsdb.TagSet
}
@@ -636,7 +629,7 @@ func (e *State) walkFunc(node *parse.FuncNode, T miniprofiler.Timer) *Results {
default:
panic(fmt.Errorf("expr: unknown func arg type"))
}
- if f, ok := v.(float64); ok && node.F.Args[i] == parse.TypeNumberSet {
+ if f, ok := v.(float64); ok && node.F.Args[i] == models.TypeNumberSet {
v = fromScalar(f)
}
in = append(in, reflect.ValueOf(v))
@@ -650,7 +643,7 @@ func (e *State) walkFunc(node *parse.FuncNode, T miniprofiler.Timer) *Results {
panic(err)
}
}
- if node.Return() == parse.TypeNumberSet {
+ if node.Return() == models.TypeNumberSet {
for _, r := range res.Results {
e.AddComputation(r, node.String(), r.Value.(Number))
}
@@ -661,13 +654,13 @@ func (e *State) walkFunc(node *parse.FuncNode, T miniprofiler.Timer) *Results {
// extract will return a float64 if res contains exactly one scalar or a ESQuery if that is the type
func extract(res *Results) interface{} {
- if len(res.Results) == 1 && res.Results[0].Type() == parse.TypeScalar {
+ if len(res.Results) == 1 && res.Results[0].Type() == models.TypeScalar {
return float64(res.Results[0].Value.Value().(Scalar))
}
- if len(res.Results) == 1 && res.Results[0].Type() == parse.TypeESQuery {
+ if len(res.Results) == 1 && res.Results[0].Type() == models.TypeESQuery {
return res.Results[0].Value.Value()
}
- if len(res.Results) == 1 && res.Results[0].Type() == parse.TypeESIndexer {
+ if len(res.Results) == 1 && res.Results[0].Type() == models.TypeESIndexer {
return res.Results[0].Value.Value()
}
return res
diff --git a/cmd/bosun/expr/funcs.go b/cmd/bosun/expr/funcs.go
index f84d413ace..b8f29351c2 100644
--- a/cmd/bosun/expr/funcs.go
+++ b/cmd/bosun/expr/funcs.go
@@ -14,6 +14,7 @@ import (
"bosun.org/_third_party/github.com/MiniProfiler/go/miniprofiler"
"bosun.org/cmd/bosun/expr/parse"
"bosun.org/graphite"
+ "bosun.org/models"
"bosun.org/opentsdb"
"bosun.org/slog"
)
@@ -79,14 +80,14 @@ func tagRename(args []parse.Node) (parse.Tags, error) {
// Graphite defines functions for use with a Graphite backend.
var Graphite = map[string]parse.Func{
"graphiteBand": {
- Args: []parse.FuncType{parse.TypeString, parse.TypeString, parse.TypeString, parse.TypeString, parse.TypeScalar},
- Return: parse.TypeSeriesSet,
+ Args: []models.FuncType{models.TypeString, models.TypeString, models.TypeString, models.TypeString, models.TypeScalar},
+ Return: models.TypeSeriesSet,
Tags: graphiteTagQuery,
F: GraphiteBand,
},
"graphite": {
- Args: []parse.FuncType{parse.TypeString, parse.TypeString, parse.TypeString, parse.TypeString},
- Return: parse.TypeSeriesSet,
+ Args: []models.FuncType{models.TypeString, models.TypeString, models.TypeString, models.TypeString},
+ Return: models.TypeSeriesSet,
Tags: graphiteTagQuery,
F: GraphiteQuery,
},
@@ -95,31 +96,31 @@ var Graphite = map[string]parse.Func{
// TSDB defines functions for use with an OpenTSDB backend.
var TSDB = map[string]parse.Func{
"band": {
- Args: []parse.FuncType{parse.TypeString, parse.TypeString, parse.TypeString, parse.TypeScalar},
- Return: parse.TypeSeriesSet,
+ Args: []models.FuncType{models.TypeString, models.TypeString, models.TypeString, models.TypeScalar},
+ Return: models.TypeSeriesSet,
Tags: tagQuery,
F: Band,
},
"change": {
- Args: []parse.FuncType{parse.TypeString, parse.TypeString, parse.TypeString},
- Return: parse.TypeNumberSet,
+ Args: []models.FuncType{models.TypeString, models.TypeString, models.TypeString},
+ Return: models.TypeNumberSet,
Tags: tagQuery,
F: Change,
},
"count": {
- Args: []parse.FuncType{parse.TypeString, parse.TypeString, parse.TypeString},
- Return: parse.TypeScalar,
+ Args: []models.FuncType{models.TypeString, models.TypeString, models.TypeString},
+ Return: models.TypeScalar,
F: Count,
},
"q": {
- Args: []parse.FuncType{parse.TypeString, parse.TypeString, parse.TypeString},
- Return: parse.TypeSeriesSet,
+ Args: []models.FuncType{models.TypeString, models.TypeString, models.TypeString},
+ Return: models.TypeSeriesSet,
Tags: tagQuery,
F: Query,
},
"window": {
- Args: []parse.FuncType{parse.TypeString, parse.TypeString, parse.TypeString, parse.TypeScalar, parse.TypeString},
- Return: parse.TypeSeriesSet,
+ Args: []models.FuncType{models.TypeString, models.TypeString, models.TypeString, models.TypeScalar, models.TypeString},
+ Return: models.TypeSeriesSet,
Tags: tagQuery,
F: Window,
Check: windowCheck,
@@ -130,191 +131,191 @@ var builtins = map[string]parse.Func{
// Reduction functions
"avg": {
- Args: []parse.FuncType{parse.TypeSeriesSet},
- Return: parse.TypeNumberSet,
+ Args: []models.FuncType{models.TypeSeriesSet},
+ Return: models.TypeNumberSet,
Tags: tagFirst,
F: Avg,
},
"cCount": {
- Args: []parse.FuncType{parse.TypeSeriesSet},
- Return: parse.TypeNumberSet,
+ Args: []models.FuncType{models.TypeSeriesSet},
+ Return: models.TypeNumberSet,
Tags: tagFirst,
F: CCount,
},
"dev": {
- Args: []parse.FuncType{parse.TypeSeriesSet},
- Return: parse.TypeNumberSet,
+ Args: []models.FuncType{models.TypeSeriesSet},
+ Return: models.TypeNumberSet,
Tags: tagFirst,
F: Dev,
},
"diff": {
- Args: []parse.FuncType{parse.TypeSeriesSet},
- Return: parse.TypeNumberSet,
+ Args: []models.FuncType{models.TypeSeriesSet},
+ Return: models.TypeNumberSet,
Tags: tagFirst,
F: Diff,
},
"first": {
- Args: []parse.FuncType{parse.TypeSeriesSet},
- Return: parse.TypeNumberSet,
+ Args: []models.FuncType{models.TypeSeriesSet},
+ Return: models.TypeNumberSet,
Tags: tagFirst,
F: First,
},
"forecastlr": {
- Args: []parse.FuncType{parse.TypeSeriesSet, parse.TypeNumberSet},
- Return: parse.TypeNumberSet,
+ Args: []models.FuncType{models.TypeSeriesSet, models.TypeNumberSet},
+ Return: models.TypeNumberSet,
Tags: tagFirst,
F: Forecast_lr,
},
"last": {
- Args: []parse.FuncType{parse.TypeSeriesSet},
- Return: parse.TypeNumberSet,
+ Args: []models.FuncType{models.TypeSeriesSet},
+ Return: models.TypeNumberSet,
Tags: tagFirst,
F: Last,
},
"len": {
- Args: []parse.FuncType{parse.TypeSeriesSet},
- Return: parse.TypeNumberSet,
+ Args: []models.FuncType{models.TypeSeriesSet},
+ Return: models.TypeNumberSet,
Tags: tagFirst,
F: Length,
},
"max": {
- Args: []parse.FuncType{parse.TypeSeriesSet},
- Return: parse.TypeNumberSet,
+ Args: []models.FuncType{models.TypeSeriesSet},
+ Return: models.TypeNumberSet,
Tags: tagFirst,
F: Max,
},
"median": {
- Args: []parse.FuncType{parse.TypeSeriesSet},
- Return: parse.TypeNumberSet,
+ Args: []models.FuncType{models.TypeSeriesSet},
+ Return: models.TypeNumberSet,
Tags: tagFirst,
F: Median,
},
"min": {
- Args: []parse.FuncType{parse.TypeSeriesSet},
- Return: parse.TypeNumberSet,
+ Args: []models.FuncType{models.TypeSeriesSet},
+ Return: models.TypeNumberSet,
Tags: tagFirst,
F: Min,
},
"percentile": {
- Args: []parse.FuncType{parse.TypeSeriesSet, parse.TypeNumberSet},
- Return: parse.TypeNumberSet,
+ Args: []models.FuncType{models.TypeSeriesSet, models.TypeNumberSet},
+ Return: models.TypeNumberSet,
Tags: tagFirst,
F: Percentile,
},
"since": {
- Args: []parse.FuncType{parse.TypeSeriesSet},
- Return: parse.TypeNumberSet,
+ Args: []models.FuncType{models.TypeSeriesSet},
+ Return: models.TypeNumberSet,
Tags: tagFirst,
F: Since,
},
"sum": {
- Args: []parse.FuncType{parse.TypeSeriesSet},
- Return: parse.TypeNumberSet,
+ Args: []models.FuncType{models.TypeSeriesSet},
+ Return: models.TypeNumberSet,
Tags: tagFirst,
F: Sum,
},
"streak": {
- Args: []parse.FuncType{parse.TypeSeriesSet},
- Return: parse.TypeNumberSet,
+ Args: []models.FuncType{models.TypeSeriesSet},
+ Return: models.TypeNumberSet,
Tags: tagFirst,
F: Streak,
},
// Group functions
"rename": {
- Args: []parse.FuncType{parse.TypeSeriesSet, parse.TypeString},
- Return: parse.TypeSeriesSet,
+ Args: []models.FuncType{models.TypeSeriesSet, models.TypeString},
+ Return: models.TypeSeriesSet,
Tags: tagRename,
F: Rename,
},
"t": {
- Args: []parse.FuncType{parse.TypeNumberSet, parse.TypeString},
- Return: parse.TypeSeriesSet,
+ Args: []models.FuncType{models.TypeNumberSet, models.TypeString},
+ Return: models.TypeSeriesSet,
Tags: tagTranspose,
F: Transpose,
},
"ungroup": {
- Args: []parse.FuncType{parse.TypeNumberSet},
- Return: parse.TypeScalar,
+ Args: []models.FuncType{models.TypeNumberSet},
+ Return: models.TypeScalar,
F: Ungroup,
},
// Other functions
"abs": {
- Args: []parse.FuncType{parse.TypeNumberSet},
- Return: parse.TypeNumberSet,
+ Args: []models.FuncType{models.TypeNumberSet},
+ Return: models.TypeNumberSet,
Tags: tagFirst,
F: Abs,
},
"d": {
- Args: []parse.FuncType{parse.TypeString},
- Return: parse.TypeScalar,
+ Args: []models.FuncType{models.TypeString},
+ Return: models.TypeScalar,
F: Duration,
},
"des": {
- Args: []parse.FuncType{parse.TypeSeriesSet, parse.TypeScalar, parse.TypeScalar},
- Return: parse.TypeSeriesSet,
+ Args: []models.FuncType{models.TypeSeriesSet, models.TypeScalar, models.TypeScalar},
+ Return: models.TypeSeriesSet,
Tags: tagFirst,
F: Des,
},
"dropge": {
- Args: []parse.FuncType{parse.TypeSeriesSet, parse.TypeNumberSet},
- Return: parse.TypeSeriesSet,
+ Args: []models.FuncType{models.TypeSeriesSet, models.TypeNumberSet},
+ Return: models.TypeSeriesSet,
Tags: tagFirst,
F: DropGe,
},
"dropg": {
- Args: []parse.FuncType{parse.TypeSeriesSet, parse.TypeNumberSet},
- Return: parse.TypeSeriesSet,
+ Args: []models.FuncType{models.TypeSeriesSet, models.TypeNumberSet},
+ Return: models.TypeSeriesSet,
Tags: tagFirst,
F: DropG,
},
"drople": {
- Args: []parse.FuncType{parse.TypeSeriesSet, parse.TypeNumberSet},
- Return: parse.TypeSeriesSet,
+ Args: []models.FuncType{models.TypeSeriesSet, models.TypeNumberSet},
+ Return: models.TypeSeriesSet,
Tags: tagFirst,
F: DropLe,
},
"dropl": {
- Args: []parse.FuncType{parse.TypeSeriesSet, parse.TypeNumberSet},
- Return: parse.TypeSeriesSet,
+ Args: []models.FuncType{models.TypeSeriesSet, models.TypeNumberSet},
+ Return: models.TypeSeriesSet,
Tags: tagFirst,
F: DropL,
},
"dropna": {
- Args: []parse.FuncType{parse.TypeSeriesSet},
- Return: parse.TypeSeriesSet,
+ Args: []models.FuncType{models.TypeSeriesSet},
+ Return: models.TypeSeriesSet,
Tags: tagFirst,
F: DropNA,
},
"epoch": {
- Args: []parse.FuncType{},
- Return: parse.TypeScalar,
+ Args: []models.FuncType{},
+ Return: models.TypeScalar,
F: Epoch,
},
"filter": {
- Args: []parse.FuncType{parse.TypeSeriesSet, parse.TypeNumberSet},
- Return: parse.TypeSeriesSet,
+ Args: []models.FuncType{models.TypeSeriesSet, models.TypeNumberSet},
+ Return: models.TypeSeriesSet,
Tags: tagFirst,
F: Filter,
},
"limit": {
- Args: []parse.FuncType{parse.TypeNumberSet, parse.TypeScalar},
- Return: parse.TypeNumberSet,
+ Args: []models.FuncType{models.TypeNumberSet, models.TypeScalar},
+ Return: models.TypeNumberSet,
Tags: tagFirst,
F: Limit,
},
"nv": {
- Args: []parse.FuncType{parse.TypeNumberSet, parse.TypeScalar},
- Return: parse.TypeNumberSet,
+ Args: []models.FuncType{models.TypeNumberSet, models.TypeScalar},
+ Return: models.TypeNumberSet,
Tags: tagFirst,
F: NV,
},
"sort": {
- Args: []parse.FuncType{parse.TypeNumberSet, parse.TypeString},
- Return: parse.TypeNumberSet,
+ Args: []models.FuncType{models.TypeNumberSet, models.TypeString},
+ Return: models.TypeNumberSet,
Tags: tagFirst,
F: Sort,
},
@@ -687,7 +688,7 @@ func windowCheck(t *parse.Tree, f *parse.FuncNode) error {
if !ok {
return fmt.Errorf("expr: Window: unknown function %v", name)
}
- if len(v.Args) != 1 || v.Args[0] != parse.TypeSeriesSet || v.Return != parse.TypeNumberSet {
+ if len(v.Args) != 1 || v.Args[0] != models.TypeSeriesSet || v.Return != models.TypeNumberSet {
return fmt.Errorf("expr: Window: %v is not a reduction function", name)
}
return nil
diff --git a/cmd/bosun/expr/influx.go b/cmd/bosun/expr/influx.go
index ff2d9661e0..a9df1fe1a4 100644
--- a/cmd/bosun/expr/influx.go
+++ b/cmd/bosun/expr/influx.go
@@ -9,16 +9,17 @@ import (
"bosun.org/_third_party/github.com/MiniProfiler/go/miniprofiler"
"bosun.org/_third_party/github.com/influxdb/influxdb/client"
"bosun.org/_third_party/github.com/influxdb/influxdb/influxql"
- "bosun.org/_third_party/github.com/influxdb/influxdb/models"
+ influxModels "bosun.org/_third_party/github.com/influxdb/influxdb/models"
"bosun.org/cmd/bosun/expr/parse"
+ "bosun.org/models"
"bosun.org/opentsdb"
)
// Influx is a map of functions to query InfluxDB.
var Influx = map[string]parse.Func{
"influx": {
- Args: []parse.FuncType{parse.TypeString, parse.TypeString, parse.TypeString, parse.TypeString, parse.TypeString},
- Return: parse.TypeSeriesSet,
+ Args: []models.FuncType{models.TypeString, models.TypeString, models.TypeString, models.TypeString, models.TypeString},
+ Return: models.TypeSeriesSet,
Tags: influxTag,
F: InfluxQuery,
},
@@ -186,7 +187,7 @@ func influxQueryDuration(now time.Time, query, start, end, groupByInterval strin
return s.String(), nil
}
-func timeInfluxRequest(e *State, T miniprofiler.Timer, db, query, startDuration, endDuration, groupByInterval string) (s []models.Row, err error) {
+func timeInfluxRequest(e *State, T miniprofiler.Timer, db, query, startDuration, endDuration, groupByInterval string) (s []influxModels.Row, err error) {
q, err := influxQueryDuration(e.now, query, startDuration, endDuration, groupByInterval)
if err != nil {
return nil, err
@@ -216,7 +217,7 @@ func timeInfluxRequest(e *State, T miniprofiler.Timer, db, query, startDuration,
var val interface{}
var ok bool
val, err = e.cache.Get(q, getFn)
- if s, ok = val.([]models.Row); !ok {
+ if s, ok = val.([]influxModels.Row); !ok {
err = fmt.Errorf("influx: did not get a valid result from InfluxDB")
}
})
diff --git a/cmd/bosun/expr/logstash.go b/cmd/bosun/expr/logstash.go
index 48aabf3973..2873ebf6b5 100644
--- a/cmd/bosun/expr/logstash.go
+++ b/cmd/bosun/expr/logstash.go
@@ -10,6 +10,7 @@ import (
"bosun.org/_third_party/github.com/MiniProfiler/go/miniprofiler"
"bosun.org/_third_party/github.com/olivere/elastic"
"bosun.org/cmd/bosun/expr/parse"
+ "bosun.org/models"
"bosun.org/opentsdb"
)
@@ -20,14 +21,14 @@ var lsClient *elastic.Client
// logstash. They are only loaded when the elastic hosts are set in the config file
var LogstashElastic = map[string]parse.Func{
"lscount": {
- Args: []parse.FuncType{parse.TypeString, parse.TypeString, parse.TypeString, parse.TypeString, parse.TypeString, parse.TypeString},
- Return: parse.TypeSeriesSet,
+ Args: []models.FuncType{models.TypeString, models.TypeString, models.TypeString, models.TypeString, models.TypeString, models.TypeString},
+ Return: models.TypeSeriesSet,
Tags: logstashTagQuery,
F: LSCount,
},
"lsstat": {
- Args: []parse.FuncType{parse.TypeString, parse.TypeString, parse.TypeString, parse.TypeString, parse.TypeString, parse.TypeString, parse.TypeString, parse.TypeString},
- Return: parse.TypeSeriesSet,
+ Args: []models.FuncType{models.TypeString, models.TypeString, models.TypeString, models.TypeString, models.TypeString, models.TypeString, models.TypeString, models.TypeString},
+ Return: models.TypeSeriesSet,
Tags: logstashTagQuery,
F: LSStat,
},
diff --git a/cmd/bosun/expr/parse/node.go b/cmd/bosun/expr/parse/node.go
index 631c991999..4c2a185088 100644
--- a/cmd/bosun/expr/parse/node.go
+++ b/cmd/bosun/expr/parse/node.go
@@ -9,6 +9,8 @@ package parse
import (
"fmt"
"strconv"
+
+ "bosun.org/models"
)
var textFormat = "%s" // Changed to "%q" in tests for better error messages.
@@ -22,7 +24,7 @@ type Node interface {
StringAST() string
Position() Pos // byte position of start of node in full original input string
Check(*Tree) error // performs type checking for itself and sub-nodes
- Return() FuncType
+ Return() models.FuncType
Tags() (Tags, error)
// Make sure only functions in this package can create Nodes.
unexported()
@@ -116,14 +118,14 @@ func (f *FuncNode) Check(t *Tree) error {
}
}
for i, arg := range f.Args {
- var funcType FuncType
+ var funcType models.FuncType
if f.F.VArgs && i >= f.F.VArgsPos {
funcType = f.F.Args[f.F.VArgsPos]
} else {
funcType = f.F.Args[i]
}
argType := arg.Return()
- if funcType == TypeNumberSet && argType == TypeScalar {
+ if funcType == models.TypeNumberSet && argType == models.TypeScalar {
// Scalars are promoted to NumberSets during execution.
} else if funcType != argType {
return fmt.Errorf("parse: expected %v, got %v for argument %v (%v)", funcType, argType, i, arg.String())
@@ -138,7 +140,7 @@ func (f *FuncNode) Check(t *Tree) error {
return nil
}
-func (f *FuncNode) Return() FuncType {
+func (f *FuncNode) Return() models.FuncType {
return f.F.Return
}
@@ -204,8 +206,8 @@ func (n *NumberNode) Check(*Tree) error {
return nil
}
-func (n *NumberNode) Return() FuncType {
- return TypeScalar
+func (n *NumberNode) Return() models.FuncType {
+ return models.TypeScalar
}
func (n *NumberNode) Tags() (Tags, error) {
@@ -236,8 +238,8 @@ func (s *StringNode) Check(*Tree) error {
return nil
}
-func (s *StringNode) Return() FuncType {
- return TypeString
+func (s *StringNode) Return() models.FuncType {
+ return models.TypeString
}
func (s *StringNode) Tags() (Tags, error) {
@@ -268,14 +270,14 @@ func (b *BinaryNode) StringAST() string {
func (b *BinaryNode) Check(t *Tree) error {
t1 := b.Args[0].Return()
t2 := b.Args[1].Return()
- if t1 == TypeSeriesSet && t2 == TypeSeriesSet {
+ if t1 == models.TypeSeriesSet && t2 == models.TypeSeriesSet {
return fmt.Errorf("parse: type error in %s: at least one side must be a number", b)
}
check := t1
- if t1 == TypeSeriesSet {
+ if t1 == models.TypeSeriesSet {
check = t2
}
- if check != TypeNumberSet && check != TypeScalar {
+ if check != models.TypeNumberSet && check != models.TypeScalar {
return fmt.Errorf("parse: type error in %s: expected a number", b)
}
if err := b.Args[0].Check(t); err != nil {
@@ -298,7 +300,7 @@ func (b *BinaryNode) Check(t *Tree) error {
return nil
}
-func (b *BinaryNode) Return() FuncType {
+func (b *BinaryNode) Return() models.FuncType {
t0 := b.Args[0].Return()
t1 := b.Args[1].Return()
if t1 > t0 {
@@ -341,14 +343,14 @@ func (u *UnaryNode) StringAST() string {
func (u *UnaryNode) Check(t *Tree) error {
switch rt := u.Arg.Return(); rt {
- case TypeNumberSet, TypeSeriesSet, TypeScalar:
+ case models.TypeNumberSet, models.TypeSeriesSet, models.TypeScalar:
return u.Arg.Check(t)
default:
return fmt.Errorf("parse: type error in %s, expected %s, got %s", u, "number", rt)
}
}
-func (u *UnaryNode) Return() FuncType {
+func (u *UnaryNode) Return() models.FuncType {
return u.Arg.Return()
}
diff --git a/cmd/bosun/expr/parse/parse.go b/cmd/bosun/expr/parse/parse.go
index 9486f631c5..0ff71e5d90 100644
--- a/cmd/bosun/expr/parse/parse.go
+++ b/cmd/bosun/expr/parse/parse.go
@@ -13,6 +13,8 @@ import (
"sort"
"strconv"
"strings"
+
+ "bosun.org/models"
)
// Tree is the representation of a single parsed expression.
@@ -29,8 +31,8 @@ type Tree struct {
}
type Func struct {
- Args []FuncType
- Return FuncType
+ Args []models.FuncType
+ Return models.FuncType
Tags func([]Node) (Tags, error)
F interface{}
VArgs bool
@@ -38,36 +40,6 @@ type Func struct {
Check func(*Tree, *FuncNode) error
}
-type FuncType int
-
-func (f FuncType) String() string {
- switch f {
- case TypeNumberSet:
- return "number"
- case TypeString:
- return "string"
- case TypeSeriesSet:
- return "series"
- case TypeScalar:
- return "scalar"
- case TypeESQuery:
- return "esquery"
- case TypeESIndexer:
- return "esindexer"
- default:
- return "unknown"
- }
-}
-
-const (
- TypeString FuncType = iota
- TypeScalar
- TypeNumberSet
- TypeSeriesSet
- TypeESQuery
- TypeESIndexer
-)
-
type Tags map[string]struct{}
func (t Tags) String() string {
@@ -214,7 +186,7 @@ func (t *Tree) startParse(funcs []map[string]Func, lex *lexer) {
for _, funcMap := range funcs {
for name, f := range funcMap {
switch f.Return {
- case TypeSeriesSet, TypeNumberSet:
+ case models.TypeSeriesSet, models.TypeNumberSet:
if f.Tags == nil {
panic(fmt.Errorf("%v: expected Tags definition: got nil", name))
}
diff --git a/cmd/bosun/expr/parse/parse_test.go b/cmd/bosun/expr/parse/parse_test.go
index 007a33fbed..ca8613d60b 100644
--- a/cmd/bosun/expr/parse/parse_test.go
+++ b/cmd/bosun/expr/parse/parse_test.go
@@ -8,6 +8,8 @@ import (
"flag"
"fmt"
"testing"
+
+ "bosun.org/models"
)
var debug = flag.Bool("debug", false, "show the errors produced by the main tests")
@@ -150,8 +152,8 @@ func tagNil(args []Node) (Tags, error) {
var builtins = map[string]Func{
"avg": {
- []FuncType{TypeSeriesSet},
- TypeNumberSet,
+ []models.FuncType{models.TypeSeriesSet},
+ models.TypeNumberSet,
tagNil,
nil,
false,
@@ -159,8 +161,8 @@ var builtins = map[string]Func{
nil,
},
"band": {
- []FuncType{TypeString, TypeString, TypeString, TypeScalar},
- TypeSeriesSet,
+ []models.FuncType{models.TypeString, models.TypeString, models.TypeString, models.TypeScalar},
+ models.TypeSeriesSet,
tagNil,
nil,
false,
@@ -168,8 +170,8 @@ var builtins = map[string]Func{
nil,
},
"q": {
- []FuncType{TypeString, TypeString},
- TypeSeriesSet,
+ []models.FuncType{models.TypeString, models.TypeString},
+ models.TypeSeriesSet,
tagNil,
nil,
false,
@@ -177,8 +179,8 @@ var builtins = map[string]Func{
nil,
},
"forecastlr": {
- []FuncType{TypeSeriesSet, TypeScalar},
- TypeNumberSet,
+ []models.FuncType{models.TypeSeriesSet, models.TypeScalar},
+ models.TypeNumberSet,
tagNil,
nil,
false,
diff --git a/cmd/bosun/sched/bolt.go b/cmd/bosun/sched/bolt.go
index 860e7e516c..c406f6ea73 100644
--- a/cmd/bosun/sched/bolt.go
+++ b/cmd/bosun/sched/bolt.go
@@ -15,6 +15,7 @@ import (
"bosun.org/_third_party/github.com/boltdb/bolt"
"bosun.org/cmd/bosun/conf"
"bosun.org/cmd/bosun/database"
+ "bosun.org/cmd/bosun/expr"
"bosun.org/collect"
"bosun.org/metadata"
"bosun.org/models"
@@ -44,7 +45,6 @@ const (
dbBucket = "bindata"
dbConfigTextBucket = "configText"
dbNotifications = "notifications"
- dbStatus = "status"
)
func (s *Schedule) save() {
@@ -54,7 +54,6 @@ func (s *Schedule) save() {
s.Lock("Save")
store := map[string]interface{}{
dbNotifications: s.Notifications,
- dbStatus: s.status,
}
tostore := make(map[string][]byte)
for name, data := range store {
@@ -141,63 +140,66 @@ func (s *Schedule) RestoreState() error {
slog.Errorln(dbNotifications, err)
}
- status := make(States)
- if err := decode(db, dbStatus, &status); err != nil {
- slog.Errorln(dbStatus, err)
- }
- clear := func(r *Result) {
- if r == nil {
- return
- }
- r.Computations = nil
- }
- for ak, st := range status {
- a, present := s.Conf.Alerts[ak.Name()]
- if !present {
- slog.Errorln("sched: alert no longer present, ignoring:", ak)
- continue
- } else if s.Conf.Squelched(a, st.Group) {
- slog.Infoln("sched: alert now squelched:", ak)
- continue
- } else {
- t := a.Unknown
- if t == 0 {
- t = s.Conf.CheckFrequency
- }
- if t == 0 && st.Last().Status == StUnknown {
- st.Append(&Event{Status: StNormal, IncidentId: st.Last().IncidentId})
- }
- }
- clear(st.Result)
- newHistory := []Event{}
- for _, e := range st.History {
- clear(e.Warn)
- clear(e.Crit)
- // Remove error events which no longer are a thing.
- if e.Status <= StUnknown {
- newHistory = append(newHistory, e)
- }
- }
- st.History = newHistory
- s.status[ak] = st
- if a.Log && st.Open {
- st.Open = false
- slog.Infof("sched: alert %s is now log, closing, was %s", ak, st.Status())
- }
- for name, t := range notifications[ak] {
- n, present := s.Conf.Notifications[name]
- if !present {
- slog.Infoln("sched: notification not present during restore:", name)
- continue
- }
- if a.Log {
- slog.Infoln("sched: alert is now log, removing notification:", ak)
- continue
- }
- s.AddNotification(ak, n, t)
- }
+ //status := make(States)
+ // if err := decode(db, dbStatus, &status); err != nil {
+ // slog.Errorln(dbStatus, err)
+ // }
+ // clear := func(r *models.Result) {
+ // if r == nil {
+ // return
+ // }
+ // r.Computations = nil
+ //}
+ //TODO: ???
+ // for ak, st := range status {
+ // a, present := s.Conf.Alerts[ak.Name()]
+ // if !present {
+ // slog.Errorln("sched: alert no longer present, ignoring:", ak)
+ // continue
+ // } else if s.Conf.Squelched(a, st.Group) {
+ // slog.Infoln("sched: alert now squelched:", ak)
+ // continue
+ // } else {
+ // t := a.Unknown
+ // if t == 0 {
+ // t = s.Conf.CheckFrequency
+ // }
+ // if t == 0 && st.Last().Status == StUnknown {
+ // st.Append(&Event{Status: StNormal, IncidentId: st.Last().IncidentId})
+ // }
+ // }
+ // clear(st.Result)
+ // newHistory := []Event{}
+ // for _, e := range st.History {
+ // clear(e.Warn)
+ // clear(e.Crit)
+ // // Remove error events which no longer are a thing.
+ // if e.Status <= StUnknown {
+ // newHistory = append(newHistory, e)
+ // }
+ // }
+ // st.History = newHistory
+ // s.status[ak] = st
+ // if a.Log && st.Open {
+ // st.Open = false
+ // slog.Infof("sched: alert %s is now log, closing, was %s", ak, st.Status())
+ // }
+ // for name, t := range notifications[ak] {
+ // n, present := s.Conf.Notifications[name]
+ // if !present {
+ // slog.Infoln("sched: notification not present during restore:", name)
+ // continue
+ // }
+ // if a.Log {
+ // slog.Infoln("sched: alert is now log, removing notification:", ak)
+ // continue
+ // }
+ // s.AddNotification(ak, n, t)
+ // }
+ //}
+ if err := migrateOldDataToRedis(db, s.DataAccess); err != nil {
+ return err
}
- migrateOldDataToRedis(db, s.DataAccess)
// delete metrictags if they exist.
deleteKey(s.db, "metrictags")
slog.Infoln("RestoreState done in", time.Since(start))
@@ -272,10 +274,10 @@ func migrateOldDataToRedis(db *bolt.DB, data database.DataAccess) error {
if err := migrateSearch(db, data); err != nil {
return err
}
- if err := migrateIncidents(db, data); err != nil {
+ if err := migrateSilence(db, data); err != nil {
return err
}
- if err := migrateSilence(db, data); err != nil {
+ if err := migrateState(db, data); err != nil {
return err
}
return nil
@@ -408,60 +410,175 @@ func migrateSearch(db *bolt.DB, data database.DataAccess) error {
return nil
}
-func migrateIncidents(db *bolt.DB, data database.DataAccess) error {
- migrated, err := isMigrated(db, "incidents")
+func migrateSilence(db *bolt.DB, data database.DataAccess) error {
+ migrated, err := isMigrated(db, "silence")
if err != nil {
return err
}
if migrated {
return nil
}
- slog.Info("migrating incidents")
- incidents := map[uint64]*models.Incident{}
- if err := decode(db, "incidents", &incidents); err != nil {
+ slog.Info("migrating silence")
+ silence := map[string]*models.Silence{}
+ if err := decode(db, "silence", &silence); err != nil {
return err
}
- max := uint64(0)
- for k, v := range incidents {
- data.Incidents().UpdateIncident(k, v)
- if k > max {
- max = k
- }
- }
-
- if err = data.Incidents().SetMaxId(max); err != nil {
- return err
+ for _, v := range silence {
+ v.TagString = v.Tags.Tags()
+ data.Silence().AddSilence(v)
}
- if err = setMigrated(db, "incidents"); err != nil {
+ if err = setMigrated(db, "silence"); err != nil {
return err
}
-
return nil
}
-func migrateSilence(db *bolt.DB, data database.DataAccess) error {
- migrated, err := isMigrated(db, "silence")
+func migrateState(db *bolt.DB, data database.DataAccess) error {
+ migrated, err := isMigrated(db, "state")
if err != nil {
return err
}
if migrated {
return nil
}
- slog.Info("migrating silence")
- silence := map[string]*models.Silence{}
- if err := decode(db, "silence", &silence); err != nil {
+ //redefine the structs as they were when we gob encoded them
+ type Result struct {
+ *expr.Result
+ Expr string
+ }
+ mResult := func(r *Result) *models.Result {
+ if r == nil || r.Result == nil {
+ return &models.Result{}
+ }
+ v, _ := valueToFloat(r.Result.Value)
+ return &models.Result{
+ Computations: r.Result.Computations,
+ Value: models.Float(v),
+ Expr: r.Expr,
+ }
+ }
+ type Event struct {
+ Warn, Crit *Result
+ Status models.Status
+ Time time.Time
+ Unevaluated bool
+ IncidentId uint64
+ }
+ type State struct {
+ *Result
+ History []Event
+ Actions []models.Action
+ Touched time.Time
+ Alert string
+ Tags string
+ Group opentsdb.TagSet
+ Subject string
+ Body string
+ EmailBody []byte
+ EmailSubject []byte
+ Attachments []*models.Attachment
+ NeedAck bool
+ Open bool
+ Forgotten bool
+ Unevaluated bool
+ LastLogTime time.Time
+ }
+ type OldStates map[models.AlertKey]*State
+ slog.Info("migrating state")
+ states := OldStates{}
+ if err := decode(db, "status", &states); err != nil {
return err
}
- for _, v := range silence {
- v.TagString = v.Tags.Tags()
- data.Silence().AddSilence(v)
+ for ak, state := range states {
+ if len(state.History) == 0 {
+ continue
+ }
+ var thisId uint64
+ events := []Event{}
+ addIncident := func(saveBody bool) error {
+ if thisId == 0 || len(events) == 0 || state == nil {
+ return nil
+ }
+ incident := NewIncident(ak)
+ incident.Expr = state.Expr
+
+ incident.NeedAck = state.NeedAck
+ incident.Open = state.Open
+ incident.Result = mResult(state.Result)
+ incident.Unevaluated = state.Unevaluated
+ incident.Start = events[0].Time
+ incident.Id = int64(thisId)
+ incident.Subject = state.Subject
+ if saveBody {
+ incident.Body = state.Body
+ }
+ for _, ev := range events {
+ incident.CurrentStatus = ev.Status
+ mEvent := models.Event{
+ Crit: mResult(ev.Crit),
+ Status: ev.Status,
+ Time: ev.Time,
+ Unevaluated: ev.Unevaluated,
+ Warn: mResult(ev.Warn),
+ }
+ incident.Events = append(incident.Events, mEvent)
+ if ev.Status > incident.WorstStatus {
+ incident.WorstStatus = ev.Status
+ }
+ if ev.Status > models.StNormal {
+ incident.LastAbnormalStatus = ev.Status
+ incident.LastAbnormalTime = ev.Time.UTC().Unix()
+ }
+ }
+ for _, ac := range state.Actions {
+ if ac.Time.Before(incident.Start) {
+ continue
+ }
+ incident.Actions = append(incident.Actions, ac)
+ if ac.Time.After(incident.Events[len(incident.Events)-1].Time) && ac.Type == models.ActionClose {
+ incident.End = &ac.Time
+ break
+ }
+ }
+ if err := data.State().ImportIncidentState(incident); err != nil {
+ return err
+ }
+ return nil
+ }
+ //essentially a rle algorithm to assign events to incidents
+ for _, e := range state.History {
+ if e.Status > models.StUnknown {
+ continue
+ }
+ if e.IncidentId == 0 {
+ //include all non-assigned incidents up to the next non-match
+ events = append(events, e)
+ continue
+ }
+ if thisId == 0 {
+ thisId = e.IncidentId
+ events = append(events, e)
+ }
+ if e.IncidentId != thisId {
+ if err := addIncident(false); err != nil {
+ return err
+ }
+ thisId = e.IncidentId
+ events = []Event{e}
+
+ } else {
+ events = append(events, e)
+ }
+ }
+ if err := addIncident(true); err != nil {
+ return err
+ }
}
- if err = setMigrated(db, "silence"); err != nil {
+ if err = setMigrated(db, "state"); err != nil {
return err
}
return nil
}
-
func isMigrated(db *bolt.DB, name string) (bool, error) {
found := false
err := db.View(func(tx *bolt.Tx) error {
diff --git a/cmd/bosun/sched/check.go b/cmd/bosun/sched/check.go
index 6124f4f536..58f4767434 100644
--- a/cmd/bosun/sched/check.go
+++ b/cmd/bosun/sched/check.go
@@ -38,41 +38,14 @@ func init() {
collect.AggregateMeta("bosun.template.render", metadata.MilliSecond, "The amount of time it takes to render the specified alert template.")
}
-func NewStatus(ak models.AlertKey) *State {
- g := ak.Group()
- return &State{
- Alert: ak.Name(),
- Tags: g.Tags(),
- Group: g,
- }
-}
-
-// Get a copy of the status for the specified alert key
-func (s *Schedule) GetStatus(ak models.AlertKey) *State {
- s.Lock("GetStatus")
- state := s.status[ak]
- if state != nil {
- state = state.Copy()
- }
- s.Unlock()
- return state
-}
-
-func (s *Schedule) SetStatus(ak models.AlertKey, st *State) {
- s.Lock("SetStatus")
- s.status[ak] = st
- s.Unlock()
-}
-
-func (s *Schedule) GetOrCreateStatus(ak models.AlertKey) *State {
- s.Lock("GetOrCreateStatus")
- state := s.status[ak]
- if state == nil {
- state = NewStatus(ak)
- s.status[ak] = state
- }
- s.Unlock()
- return state
+func NewIncident(ak models.AlertKey) *models.IncidentState {
+ s := &models.IncidentState{}
+ s.Start = time.Now()
+ s.AlertKey = ak
+ s.Alert = ak.Name()
+ s.Tags = ak.Group().Tags()
+ s.Result = &models.Result{}
+ return s
}
type RunHistory struct {
@@ -84,7 +57,7 @@ type RunHistory struct {
Logstash expr.LogstashElasticHosts
Elastic expr.ElasticHosts
- Events map[models.AlertKey]*Event
+ Events map[models.AlertKey]*models.Event
schedule *Schedule
}
@@ -100,7 +73,7 @@ func (s *Schedule) NewRunHistory(start time.Time, cache *cache.Cache) *RunHistor
return &RunHistory{
Cache: cache,
Start: start,
- Events: make(map[models.AlertKey]*Event),
+ Events: make(map[models.AlertKey]*models.Event),
Context: s.Conf.TSDBContext(),
GraphiteContext: s.Conf.GraphiteContext(),
InfluxConfig: s.Conf.InfluxConfig,
@@ -115,7 +88,11 @@ func (s *Schedule) RunHistory(r *RunHistory) {
checkNotify := false
silenced := s.Silenced()
for ak, event := range r.Events {
- checkNotify = s.runHistory(r, ak, event, silenced) || checkNotify
+ shouldNotify, err := s.runHistory(r, ak, event, silenced)
+ checkNotify = checkNotify || shouldNotify
+ if err != nil {
+ slog.Errorf("Error in runHistory for %s. %s.", ak, err)
+ }
}
if checkNotify && s.nc != nil {
select {
@@ -126,146 +103,148 @@ func (s *Schedule) RunHistory(r *RunHistory) {
}
// RunHistory for a single alert key. Returns true if notifications were altered.
-func (s *Schedule) runHistory(r *RunHistory, ak models.AlertKey, event *Event, silenced map[models.AlertKey]models.Silence) bool {
- checkNotify := false
- // get existing state object for alert key. add to schedule status if doesn't already exist
- state := s.GetStatus(ak)
- if state == nil {
- state = NewStatus(ak)
- s.SetStatus(ak, state)
- }
- defer s.SetStatus(ak, state)
- // make sure we always touch the state.
- state.Touched = r.Start
- // set state.Result according to event result
- if event.Crit != nil {
- state.Result = event.Crit
- } else if event.Warn != nil {
- state.Result = event.Warn
- }
- // if event is unevaluated, we are done.
- state.Unevaluated = event.Unevaluated
- if event.Unevaluated {
- return checkNotify
- }
- // assign incident id to new event if applicable
- prev := state.Last()
- worst := StNormal
+func (s *Schedule) runHistory(r *RunHistory, ak models.AlertKey, event *models.Event, silenced SilenceTester) (checkNotify bool, err error) {
event.Time = r.Start
- if prev.IncidentId != 0 {
- // If last event has incident id and is not closed, we continue it.
- incident, err := s.DataAccess.Incidents().GetIncident(prev.IncidentId)
- if err != nil {
- slog.Error(err)
- } else if incident.End == nil {
- event.IncidentId = prev.IncidentId
- worst = state.WorstThisIncident()
- }
+ data := s.DataAccess.State()
+ err = data.TouchAlertKey(ak, time.Now())
+ if err != nil {
+ return
}
- if event.IncidentId == 0 && event.Status != StNormal {
- incident, err := s.createIncident(ak, event.Time)
- if err != nil {
- slog.Error("Error creating incident", err)
+ // get existing open incident if exists
+ incident, err := data.GetOpenIncident(ak)
+ if err != nil {
+ return
+ }
+ defer func() {
+ // save unless incident is new and closed (log alert)
+ if incident != nil && (incident.Id != 0 || incident.Open) {
+ err = data.UpdateIncidentState(incident)
} else {
- event.IncidentId = incident.Id
+ err = data.SetUnevaluated(ak, event.Unevaluated) // if nothing to save, at least store the unevaluated state
}
+ }()
+ // If nothing is out of the ordinary we are done
+ if event.Status <= models.StNormal && incident == nil {
+ return
+ }
+
+ // if event is unevaluated, we are done also.
+ if incident != nil {
+ incident.Unevaluated = event.Unevaluated
+ }
+ if event.Unevaluated {
+ return
+ }
+
+ shouldNotify := false
+ if incident == nil {
+ incident = NewIncident(ak)
+ shouldNotify = true
+ }
+ // set state.Result according to event result
+ if event.Status == models.StCritical {
+ incident.Result = event.Crit
+ } else if event.Status == models.StWarning {
+ incident.Result = event.Warn
+ }
+
+ if event.Status > models.StNormal {
+ incident.LastAbnormalStatus = event.Status
+ incident.LastAbnormalTime = event.Time.UTC().Unix()
+ }
+ if event.Status > incident.WorstStatus {
+ incident.WorstStatus = event.Status
+ shouldNotify = true
}
+ if event.Status != incident.CurrentStatus {
+ incident.Events = append(incident.Events, *event)
+ }
+ incident.CurrentStatus = event.Status
- state.Append(event)
a := s.Conf.Alerts[ak.Name()]
- // render templates and open alert key if abnormal
- if event.Status > StNormal {
- s.executeTemplates(state, event, a, r)
- state.Open = true
+ //render templates and open alert key if abnormal
+ if event.Status > models.StNormal {
+ s.executeTemplates(incident, event, a, r)
+ incident.Open = true
if a.Log {
- worst = StNormal
- state.Open = false
+ incident.Open = false
}
}
+
// On state increase, clear old notifications and notify current.
- // If the old alert was not acknowledged, do nothing.
// Do nothing if state did not change.
notify := func(ns *conf.Notifications) {
if a.Log {
- lastLogTime := state.LastLogTime
+ lastLogTime := s.lastLogTimes[ak]
now := time.Now()
if now.Before(lastLogTime.Add(a.MaxLogFrequency)) {
return
}
- state.LastLogTime = now
+ s.lastLogTimes[ak] = now
}
- nots := ns.Get(s.Conf, state.Group)
+ nots := ns.Get(s.Conf, incident.AlertKey.Group())
for _, n := range nots {
- s.Notify(state, n)
+ s.Notify(incident, n)
checkNotify = true
}
}
+
notifyCurrent := func() {
- // Auto close ignoreUnknowns.
- if a.IgnoreUnknown && event.Status == StUnknown {
- state.Open = false
- state.Forgotten = true
- state.NeedAck = false
- state.Action("bosun", "Auto close because alert has ignoreUnknown.", ActionClose, event.Time)
- slog.Infof("auto close %s because alert has ignoreUnknown", ak)
+ si := silenced(ak)
+ //Auto close ignoreUnknowns for new incident.
+ if a.IgnoreUnknown && event.Status == models.StUnknown {
+ incident.Open = false
return
- } else if silenced[ak].Forget && event.Status == StUnknown {
- state.Open = false
- state.Forgotten = true
- state.NeedAck = false
- state.Action("bosun", "Auto close because alert is silenced and marked auto forget.", ActionClose, event.Time)
- slog.Infof("auto close %s because alert is silenced and marked auto forget", ak)
+ } else if si != nil && si.Forget && event.Status == models.StUnknown {
+ incident.Open = false
return
}
- state.NeedAck = true
+ incident.NeedAck = true
switch event.Status {
- case StCritical, StUnknown:
+ case models.StCritical, models.StUnknown:
notify(a.CritNotification)
- case StWarning:
+ case models.StWarning:
notify(a.WarnNotification)
}
}
clearOld := func() {
- state.NeedAck = false
+ incident.NeedAck = false
delete(s.Notifications, ak)
}
// lock while we change notifications.
s.Lock("RunHistory")
- if event.Status > worst {
+ if shouldNotify {
clearOld()
notifyCurrent()
- } else if _, ok := silenced[ak]; ok && event.Status == StNormal {
+ }
+
+ // finally close an open alert with silence once it goes back to normal.
+ if si := silenced(ak); si != nil && event.Status == models.StNormal {
go func(ak models.AlertKey) {
slog.Infof("auto close %s because was silenced", ak)
- err := s.Action("bosun", "Auto close because was silenced.", ActionClose, ak)
+ err := s.Action("bosun", "Auto close because was silenced.", models.ActionClose, ak)
if err != nil {
slog.Errorln(err)
}
}(ak)
}
-
s.Unlock()
- return checkNotify
+ return checkNotify, nil
}
-func (s *Schedule) executeTemplates(state *State, event *Event, a *conf.Alert, r *RunHistory) {
- state.Subject = ""
- state.Body = ""
- state.EmailBody = nil
- state.EmailSubject = nil
- state.Attachments = nil
- if event.Status != StUnknown {
+func (s *Schedule) executeTemplates(state *models.IncidentState, event *models.Event, a *conf.Alert, r *RunHistory) {
+ if event.Status != models.StUnknown {
var errs []error
metric := "template.render"
//Render subject
endTiming := collect.StartTimer(metric, opentsdb.TagSet{"alert": a.Name, "type": "subject"})
subject, err := s.ExecuteSubject(r, a, state, false)
if err != nil {
- slog.Infof("%s: %v", state.AlertKey(), err)
+ slog.Infof("%s: %v", state.AlertKey, err)
errs = append(errs, err)
} else if subject == nil {
- err = fmt.Errorf("Empty subject on %s", state.AlertKey())
+ err = fmt.Errorf("Empty subject on %s", state.AlertKey)
slog.Error(err)
errs = append(errs, err)
}
@@ -275,10 +254,10 @@ func (s *Schedule) executeTemplates(state *State, event *Event, a *conf.Alert, r
endTiming = collect.StartTimer(metric, opentsdb.TagSet{"alert": a.Name, "type": "body"})
body, _, err := s.ExecuteBody(r, a, state, false)
if err != nil {
- slog.Infof("%s: %v", state.AlertKey(), err)
+ slog.Infof("%s: %v", state.AlertKey, err)
errs = append(errs, err)
} else if subject == nil {
- err = fmt.Errorf("Empty body on %s", state.AlertKey())
+ err = fmt.Errorf("Empty body on %s", state.AlertKey)
slog.Error(err)
errs = append(errs, err)
}
@@ -288,10 +267,10 @@ func (s *Schedule) executeTemplates(state *State, event *Event, a *conf.Alert, r
endTiming = collect.StartTimer(metric, opentsdb.TagSet{"alert": a.Name, "type": "emailbody"})
emailbody, attachments, err := s.ExecuteBody(r, a, state, true)
if err != nil {
- slog.Infof("%s: %v", state.AlertKey(), err)
+ slog.Infof("%s: %v", state.AlertKey, err)
errs = append(errs, err)
} else if subject == nil {
- err = fmt.Errorf("Empty email body on %s", state.AlertKey())
+ err = fmt.Errorf("Empty email body on %s", state.AlertKey)
slog.Error(err)
errs = append(errs, err)
}
@@ -301,10 +280,10 @@ func (s *Schedule) executeTemplates(state *State, event *Event, a *conf.Alert, r
endTiming = collect.StartTimer(metric, opentsdb.TagSet{"alert": a.Name, "type": "emailsubject"})
emailsubject, err := s.ExecuteSubject(r, a, state, true)
if err != nil {
- slog.Infof("%s: %v", state.AlertKey(), err)
+ slog.Infof("%s: %v", state.AlertKey, err)
errs = append(errs, err)
} else if subject == nil {
- err = fmt.Errorf("Empty email subject on %s", state.AlertKey())
+ err = fmt.Errorf("Empty email subject on %s", state.AlertKey)
slog.Error(err)
errs = append(errs, err)
}
@@ -342,7 +321,7 @@ func (s *Schedule) CollectStates() {
for _, alert := range s.Conf.Alerts {
severityCounts[alert.Name] = make(map[string]int64)
abnormalCounts[alert.Name] = make(map[string]int64)
- var i Status
+ var i models.Status
for i = 1; i.String() != "none"; i++ {
severityCounts[alert.Name][i.String()] = 0
abnormalCounts[alert.Name][i.String()] = 0
@@ -360,36 +339,37 @@ func (s *Schedule) CollectStates() {
ackByNotificationCounts[notificationName][false] = 0
ackByNotificationCounts[notificationName][true] = 0
}
- for _, state := range s.status {
- if !state.Open {
- continue
- }
- name := state.AlertKey().Name()
- alertDef := s.Conf.Alerts[name]
- nots := make(map[string]bool)
- for name := range alertDef.WarnNotification.Get(s.Conf, state.Group) {
- nots[name] = true
- }
- for name := range alertDef.CritNotification.Get(s.Conf, state.Group) {
- nots[name] = true
- }
- incident, err := s.GetIncident(state.Last().IncidentId)
- if err != nil {
- slog.Errorln(err)
- }
- for notificationName := range nots {
- ackByNotificationCounts[notificationName][state.NeedAck]++
- if incident != nil && incident.Start.Before(unAckOldestByNotification[notificationName]) && state.NeedAck {
- unAckOldestByNotification[notificationName] = incident.Start
- }
- }
- severity := state.Status().String()
- lastAbnormal := state.AbnormalStatus().String()
- severityCounts[state.Alert][severity]++
- abnormalCounts[state.Alert][lastAbnormal]++
- ackStatusCounts[state.Alert][state.NeedAck]++
- activeStatusCounts[state.Alert][state.IsActive()]++
- }
+ //TODO:
+ // for _, state := range s.status {
+ // if !state.Open {
+ // continue
+ // }
+ // name := state.AlertKey.Name()
+ // alertDef := s.Conf.Alerts[name]
+ // nots := make(map[string]bool)
+ // for name := range alertDef.WarnNotification.Get(s.Conf, state.Group) {
+ // nots[name] = true
+ // }
+ // for name := range alertDef.CritNotification.Get(s.Conf, state.Group) {
+ // nots[name] = true
+ // }
+ // incident, err := s.GetIncident(state.Last().IncidentId)
+ // if err != nil {
+ // slog.Errorln(err)
+ // }
+ // for notificationName := range nots {
+ // ackByNotificationCounts[notificationName][state.NeedAck]++
+ // if incident != nil && incident.Start.Before(unAckOldestByNotification[notificationName]) && state.NeedAck {
+ // unAckOldestByNotification[notificationName] = incident.Start
+ // }
+ // }
+ // severity := state.CurrentStatus.String()
+ // lastAbnormal := state.LastAbnormalStatus.String()
+ // severityCounts[state.Alert][severity]++
+ // abnormalCounts[state.Alert][lastAbnormal]++
+ // ackStatusCounts[state.Alert][state.NeedAck]++
+ // activeStatusCounts[state.Alert][state.IsActive()]++
+ // }
for notification := range ackByNotificationCounts {
ts := opentsdb.TagSet{"notification": notification}
err := collect.Put("alerts.acknowledgement_status_by_notification",
@@ -462,21 +442,12 @@ func (s *Schedule) CollectStates() {
}
}
-func (r *RunHistory) GetUnknownAndUnevaluatedAlertKeys(alert string) (unknown, uneval []models.AlertKey) {
- unknown = []models.AlertKey{}
- uneval = []models.AlertKey{}
- r.schedule.Lock("GetUnknownUneval")
- for ak, st := range r.schedule.status {
- if ak.Name() != alert {
- continue
- }
- if st.Last().Status == StUnknown {
- unknown = append(unknown, ak)
- } else if st.Unevaluated {
- uneval = append(uneval, ak)
- }
+func (s *Schedule) GetUnknownAndUnevaluatedAlertKeys(alert string) (unknown, uneval []models.AlertKey) {
+ unknown, uneval, err := s.DataAccess.State().GetUnknownAndUnevalAlertKeys(alert)
+ if err != nil {
+ slog.Errorf("Error getting unknown/unevaluated alert keys: %s", err)
+ return nil, nil
}
- r.schedule.Unlock()
return unknown, uneval
}
@@ -487,23 +458,23 @@ func (s *Schedule) findUnknownAlerts(now time.Time, alert string) []models.Alert
if time.Now().Sub(bosunStartupTime) < s.Conf.CheckFrequency {
return keys
}
- s.Lock("FindUnknown")
- for ak, st := range s.status {
- name := ak.Name()
- if name != alert || st.Forgotten || !s.AlertSuccessful(ak.Name()) {
- continue
- }
- a := s.Conf.Alerts[name]
- t := a.Unknown
- if t == 0 {
- t = s.Conf.CheckFrequency * 2 * time.Duration(a.RunEvery)
- }
- if now.Sub(st.Touched) < t {
- continue
- }
+ if !s.AlertSuccessful(alert) {
+ return keys
+ }
+ a := s.Conf.Alerts[alert]
+ t := a.Unknown
+ if t == 0 {
+ t = s.Conf.CheckFrequency * 2 * time.Duration(a.RunEvery)
+ }
+ maxTouched := now.UTC().Unix() - int64(t.Seconds())
+ untouched, err := s.DataAccess.State().GetUntouchedSince(alert, maxTouched)
+ if err != nil {
+ slog.Errorf("Error finding unknown alerts for alert %s: %s.", alert, err)
+ return keys
+ }
+ for _, ak := range untouched {
keys = append(keys, ak)
}
- s.Unlock()
return keys
}
@@ -511,16 +482,16 @@ func (s *Schedule) CheckAlert(T miniprofiler.Timer, r *RunHistory, a *conf.Alert
slog.Infof("check alert %v start", a.Name)
start := time.Now()
for _, ak := range s.findUnknownAlerts(r.Start, a.Name) {
- r.Events[ak] = &Event{Status: StUnknown}
+ r.Events[ak] = &models.Event{Status: models.StUnknown}
}
var warns, crits models.AlertKeys
d, err := s.executeExpr(T, r, a, a.Depends)
var deps expr.ResultSlice
if err == nil {
deps = filterDependencyResults(d)
- crits, err = s.CheckExpr(T, r, a, a.Crit, StCritical, nil)
+ crits, err = s.CheckExpr(T, r, a, a.Crit, models.StCritical, nil)
if err == nil {
- warns, err = s.CheckExpr(T, r, a, a.Warn, StWarning, crits)
+ warns, err = s.CheckExpr(T, r, a, a.Warn, models.StWarning, crits)
}
}
unevalCount, unknownCount := markDependenciesUnevaluated(r.Events, deps, a.Name)
@@ -535,9 +506,9 @@ func (s *Schedule) CheckAlert(T miniprofiler.Timer, r *RunHistory, a *conf.Alert
slog.Infof("check alert %v done (%s): %v crits, %v warns, %v unevaluated, %v unknown", a.Name, time.Since(start), len(crits), len(warns), unevalCount, unknownCount)
}
-func removeUnknownEvents(evs map[models.AlertKey]*Event, alert string) {
+func removeUnknownEvents(evs map[models.AlertKey]*models.Event, alert string) {
for k, v := range evs {
- if v.Status == StUnknown && k.Name() == alert {
+ if v.Status == models.StUnknown && k.Name() == alert {
delete(evs, k)
}
}
@@ -565,7 +536,7 @@ func filterDependencyResults(results *expr.Results) expr.ResultSlice {
return filtered
}
-func markDependenciesUnevaluated(events map[models.AlertKey]*Event, deps expr.ResultSlice, alert string) (unevalCount, unknownCount int) {
+func markDependenciesUnevaluated(events map[models.AlertKey]*models.Event, deps expr.ResultSlice, alert string) (unevalCount, unknownCount int) {
for ak, ev := range events {
if ak.Name() != alert {
continue
@@ -575,7 +546,7 @@ func markDependenciesUnevaluated(events map[models.AlertKey]*Event, deps expr.Re
ev.Unevaluated = true
unevalCount++
}
- if ev.Status == StUnknown {
+ if ev.Status == models.StUnknown {
unknownCount++
}
}
@@ -587,11 +558,11 @@ func (s *Schedule) executeExpr(T miniprofiler.Timer, rh *RunHistory, a *conf.Ale
if e == nil {
return nil, nil
}
- results, _, err := e.Execute(rh.Context, rh.GraphiteContext, rh.Logstash, rh.Elastic, rh.InfluxConfig, rh.Cache, T, rh.Start, 0, a.UnjoinedOK, s.Search, s.Conf.AlertSquelched(a), rh)
+ results, _, err := e.Execute(rh.Context, rh.GraphiteContext, rh.Logstash, rh.Elastic, rh.InfluxConfig, rh.Cache, T, rh.Start, 0, a.UnjoinedOK, s.Search, s.Conf.AlertSquelched(a), s)
return results, err
}
-func (s *Schedule) CheckExpr(T miniprofiler.Timer, rh *RunHistory, a *conf.Alert, e *expr.Expr, checkStatus Status, ignore models.AlertKeys) (alerts models.AlertKeys, err error) {
+func (s *Schedule) CheckExpr(T miniprofiler.Timer, rh *RunHistory, a *conf.Alert, e *expr.Expr, checkStatus models.Status, ignore models.AlertKeys) (alerts models.AlertKeys, err error) {
if e == nil {
return
}
@@ -618,37 +589,33 @@ Loop:
}
}
var n float64
- switch v := r.Value.(type) {
- case expr.Number:
- n = float64(v)
- case expr.Scalar:
- n = float64(v)
- default:
- err = fmt.Errorf("expected number or scalar")
+ n, err = valueToFloat(r.Value)
+ if err != nil {
return
}
event := rh.Events[ak]
if event == nil {
- event = new(Event)
+ event = new(models.Event)
rh.Events[ak] = event
}
- result := &Result{
- Result: r,
- Expr: e.String(),
+ result := &models.Result{
+ Computations: r.Computations,
+ Value: models.Float(n),
+ Expr: e.String(),
}
switch checkStatus {
- case StWarning:
+ case models.StWarning:
event.Warn = result
- case StCritical:
+ case models.StCritical:
event.Crit = result
}
status := checkStatus
if math.IsNaN(n) {
status = checkStatus
} else if n == 0 {
- status = StNormal
+ status = models.StNormal
}
- if status != StNormal {
+ if status != models.StNormal {
alerts = append(alerts, ak)
}
if status > rh.Events[ak].Status {
@@ -657,3 +624,16 @@ Loop:
}
return
}
+
+func valueToFloat(val expr.Value) (float64, error) {
+ var n float64
+ switch v := val.(type) {
+ case expr.Number:
+ n = float64(v)
+ case expr.Scalar:
+ n = float64(v)
+ default:
+ return 0, fmt.Errorf("expected number or scalar")
+ }
+ return n, nil
+}
diff --git a/cmd/bosun/sched/check_test.go b/cmd/bosun/sched/check_test.go
index 271218a61a..f9c68c57e9 100644
--- a/cmd/bosun/sched/check_test.go
+++ b/cmd/bosun/sched/check_test.go
@@ -15,7 +15,7 @@ import (
)
func TestCheckFlapping(t *testing.T) {
-
+ defer setup()()
c, err := conf.New("", `
template t {
subject = 1
@@ -37,8 +37,8 @@ func TestCheckFlapping(t *testing.T) {
s, _ := initSched(c)
ak := models.NewAlertKey("a", nil)
r := &RunHistory{
- Events: map[models.AlertKey]*Event{
- ak: {Status: StWarning},
+ Events: map[models.AlertKey]*models.Event{
+ ak: {Status: models.StWarning},
},
}
hasNots := func() bool {
@@ -58,17 +58,17 @@ func TestCheckFlapping(t *testing.T) {
}
type stateTransition struct {
- S Status
+ S models.Status
ExpectNots bool
}
transitions := []stateTransition{
- {StWarning, true},
- {StNormal, false},
- {StWarning, false},
- {StNormal, false},
- {StCritical, true},
- {StWarning, false},
- {StCritical, false},
+ {models.StWarning, true},
+ {models.StNormal, false},
+ {models.StWarning, false},
+ {models.StNormal, false},
+ {models.StCritical, true},
+ {models.StWarning, false},
+ {models.StCritical, false},
}
for i, trans := range transitions {
@@ -81,13 +81,13 @@ func TestCheckFlapping(t *testing.T) {
t.Fatalf("expected notifications for transition %d.", i)
}
}
- r.Events[ak].Status = StNormal
+ r.Events[ak].Status = models.StNormal
s.RunHistory(r)
// Close the alert, so it should notify next time.
- if err := s.Action("", "", ActionClose, ak); err != nil {
+ if err := s.Action("", "", models.ActionClose, ak); err != nil {
t.Fatal(err)
}
- r.Events[ak].Status = StWarning
+ r.Events[ak].Status = models.StWarning
s.RunHistory(r)
if !hasNots() {
t.Fatal("expected notification")
@@ -95,7 +95,7 @@ func TestCheckFlapping(t *testing.T) {
}
func TestCheckSilence(t *testing.T) {
-
+ defer setup()()
done := make(chan bool, 1)
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
done <- true
@@ -141,6 +141,7 @@ func TestCheckSilence(t *testing.T) {
}
func TestIncidentIds(t *testing.T) {
+ defer setup()()
c, err := conf.New("", `
alert a {
crit = 1
@@ -152,42 +153,43 @@ func TestIncidentIds(t *testing.T) {
s, _ := initSched(c)
ak := models.NewAlertKey("a", nil)
r := &RunHistory{
- Events: map[models.AlertKey]*Event{
- ak: {Status: StWarning},
+ Events: map[models.AlertKey]*models.Event{
+ ak: {Status: models.StWarning},
},
}
- expect := func(id uint64) {
- if s.status[ak].Last().IncidentId != id {
- t.Fatalf("Expeted incident id %d. Got %d.", id, s.status[ak].Last().IncidentId)
+ expect := func(id int64) {
+ incident, err := s.DataAccess.State().GetLatestIncident(ak)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if incident.Id != id {
+ t.Fatalf("Expeted incident id %d. Got %d.", id, incident.Id)
}
}
s.RunHistory(r)
expect(1)
- r.Events[ak].Status = StNormal
- r.Events[ak].IncidentId = 0
+ r.Events[ak].Status = models.StNormal
s.RunHistory(r)
expect(1)
- r.Events[ak].Status = StWarning
- r.Events[ak].IncidentId = 0
+ r.Events[ak].Status = models.StWarning
s.RunHistory(r)
expect(1)
- r.Events[ak].Status = StNormal
- r.Events[ak].IncidentId = 0
+ r.Events[ak].Status = models.StNormal
s.RunHistory(r)
- err = s.Action("", "", ActionClose, ak)
+ err = s.Action("", "", models.ActionClose, ak)
if err != nil {
t.Fatal(err)
}
- r.Events[ak].Status = StWarning
- r.Events[ak].IncidentId = 0
+ r.Events[ak].Status = models.StWarning
s.RunHistory(r)
expect(2)
}
func TestCheckNotify(t *testing.T) {
+ defer setup()()
nc := make(chan string)
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
b, _ := ioutil.ReadAll(r.Body)
@@ -231,6 +233,7 @@ func TestCheckNotify(t *testing.T) {
}
func TestCheckNotifyUnknown(t *testing.T) {
+ defer setup()()
nc := make(chan string, 1)
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
b, _ := ioutil.ReadAll(r.Body)
@@ -264,9 +267,9 @@ func TestCheckNotifyUnknown(t *testing.T) {
t.Fatal(err)
}
r := &RunHistory{
- Events: map[models.AlertKey]*Event{
- models.NewAlertKey("a", opentsdb.TagSet{"h": "x"}): {Status: StUnknown},
- models.NewAlertKey("a", opentsdb.TagSet{"h": "y"}): {Status: StUnknown},
+ Events: map[models.AlertKey]*models.Event{
+ models.NewAlertKey("a", opentsdb.TagSet{"h": "x"}): {Status: models.StUnknown},
+ models.NewAlertKey("a", opentsdb.TagSet{"h": "y"}): {Status: models.StUnknown},
},
}
s.RunHistory(r)
@@ -294,6 +297,7 @@ Loop:
// TestCheckNotifyUnknownDefault tests the default unknownTemplate.
func TestCheckNotifyUnknownDefault(t *testing.T) {
+ defer setup()()
nc := make(chan string, 1)
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
b, _ := ioutil.ReadAll(r.Body)
@@ -326,9 +330,9 @@ func TestCheckNotifyUnknownDefault(t *testing.T) {
t.Fatal(err)
}
r := &RunHistory{
- Events: map[models.AlertKey]*Event{
- models.NewAlertKey("a", opentsdb.TagSet{"h": "x"}): {Status: StUnknown},
- models.NewAlertKey("a", opentsdb.TagSet{"h": "y"}): {Status: StUnknown},
+ Events: map[models.AlertKey]*models.Event{
+ models.NewAlertKey("a", opentsdb.TagSet{"h": "x"}): {Status: models.StUnknown},
+ models.NewAlertKey("a", opentsdb.TagSet{"h": "y"}): {Status: models.StUnknown},
},
}
s.RunHistory(r)
@@ -355,6 +359,7 @@ Loop:
}
func TestCheckNotifyLog(t *testing.T) {
+ defer setup()()
nc := make(chan string, 1)
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
b, _ := ioutil.ReadAll(r.Body)
@@ -417,8 +422,12 @@ Loop:
if !gotB {
t.Errorf("didn't get expected b")
}
- for ak, st := range s.status {
- switch ak {
+ status, err := s.DataAccess.State().GetAllOpenIncidents()
+ if err != nil {
+ t.Fatal(err)
+ }
+ for _, st := range status {
+ switch st.AlertKey {
case "a{}":
if !st.Open {
t.Errorf("expected a to be open")
@@ -428,56 +437,7 @@ Loop:
t.Errorf("expected b to be closed")
}
default:
- t.Errorf("unexpected alert key %s", ak)
+ t.Errorf("unexpected alert key %s", st.AlertKey)
}
}
}
-
-// TestCheckCritUnknownEmpty checks that if an alert goes normal -> crit ->
-// unknown, it's body and subject are empty. This is because we should not
-// keep around the crit template renders if we are unknown.
-func TestCheckCritUnknownEmpty(t *testing.T) {
- c, err := conf.New("", `
- template t {
- subject = 1
- body = 2
- }
- alert a {
- crit = 1
- template = t
- }
- `)
- if err != nil {
- t.Fatal(err)
- }
- s, _ := initSched(c)
- ak := models.NewAlertKey("a", nil)
- r := &RunHistory{
- Events: map[models.AlertKey]*Event{
- ak: {Status: StNormal},
- },
- }
- verify := func(empty bool) {
- st := s.GetStatus(ak)
- if empty {
- if st.Body != "" || st.Subject != "" {
- t.Fatalf("expected empty body and subject")
- }
- } else {
- if st.Body != "
2" || st.Subject != "1" {
- t.Fatalf("expected body and subject")
- }
- }
- }
- s.RunHistory(r)
- verify(true)
- r.Events[ak].Status = StCritical
- s.RunHistory(r)
- verify(false)
- r.Events[ak].Status = StUnknown
- s.RunHistory(r)
- verify(true)
- r.Events[ak].Status = StNormal
- s.RunHistory(r)
- verify(true)
-}
diff --git a/cmd/bosun/sched/depends_test.go b/cmd/bosun/sched/depends_test.go
index 11e31a0262..01d5c79591 100644
--- a/cmd/bosun/sched/depends_test.go
+++ b/cmd/bosun/sched/depends_test.go
@@ -11,6 +11,7 @@ import (
// Crit returns {a=b},{a=c}, but {a=b} is ignored by dependency expression.
// Result should be {a=c} only.
func TestDependency_Simple(t *testing.T) {
+ defer setup()()
testSched(t, &schedTest{
conf: `alert a {
crit = avg(q("avg:c{a=*}", "5m", "")) > 0
@@ -50,6 +51,7 @@ func TestDependency_Simple(t *testing.T) {
// Crit and depends don't have same tag sets.
func TestDependency_Overlap(t *testing.T) {
+ defer setup()()
testSched(t, &schedTest{
conf: `alert a {
crit = avg(q("avg:c{a=*,b=*}", "5m", "")) > 0
@@ -88,6 +90,7 @@ func TestDependency_Overlap(t *testing.T) {
}
func TestDependency_OtherAlert(t *testing.T) {
+ defer setup()()
testSched(t, &schedTest{
conf: `alert a {
crit = avg(q("avg:a{host=*,cpu=*}", "5m", "")) > 0
@@ -129,9 +132,7 @@ func TestDependency_OtherAlert(t *testing.T) {
}
func TestDependency_OtherAlert_Unknown(t *testing.T) {
- state := NewStatus("a{host=ny02}")
- state.Touched = queryTime.Add(-10 * time.Minute)
- state.Append(&Event{Status: StNormal, Time: state.Touched})
+ defer setup()()
testSched(t, &schedTest{
conf: `alert a {
@@ -169,25 +170,18 @@ func TestDependency_OtherAlert_Unknown(t *testing.T) {
schedState{"a{host=ny02}", "unknown"}: true,
schedState{"os.cpu{host=ny01}", "warning"}: true,
},
- previous: map[models.AlertKey]*State{
- "a{host=ny02}": state,
+ touched: map[models.AlertKey]time.Time{
+ "a{host=ny02}": queryTime.Add(-10 * time.Minute),
},
})
}
func TestDependency_OtherAlert_UnknownChain(t *testing.T) {
+ defer setup()()
ab := models.AlertKey("a{host=b}")
bb := models.AlertKey("b{host=b}")
cb := models.AlertKey("c{host=b}")
- as := NewStatus(ab)
- as.Touched = queryTime.Add(-time.Hour)
- as.Append(&Event{Status: StNormal})
- bs := NewStatus(ab)
- bs.Touched = queryTime
- bs.Append(&Event{Status: StNormal})
- cs := NewStatus(ab)
- cs.Touched = queryTime
- cs.Append(&Event{Status: StNormal})
+
s := testSched(t, &schedTest{
conf: `
alert a {
@@ -215,28 +209,37 @@ func TestDependency_OtherAlert_UnknownChain(t *testing.T) {
state: map[schedState]bool{
schedState{string(ab), "unknown"}: true,
},
- previous: map[models.AlertKey]*State{
- ab: as,
- bb: bs,
- cb: cs,
+ touched: map[models.AlertKey]time.Time{
+ ab: queryTime.Add(-time.Hour),
+ bb: queryTime,
+ cb: queryTime,
},
})
- if s.status[ab].Unevaluated {
- t.Errorf("should not be unevaluated: %s", ab)
- }
- if !s.status[bb].Unevaluated {
- t.Errorf("should be unevaluated: %s", bb)
- }
- if !s.status[cb].Unevaluated {
- t.Errorf("should be unevaluated: %s", cb)
+ check := func(ak models.AlertKey, expec bool) {
+ _, uneval, err := s.DataAccess.State().GetUnknownAndUnevalAlertKeys(ak.Name())
+ if err != nil {
+ t.Fatal(err)
+ }
+ for _, ak2 := range uneval {
+ if ak2 == ak {
+ if !expec {
+ t.Fatalf("Should not be unevaluated: %s", ak)
+ } else {
+ return
+ }
+ }
+ }
+ if expec {
+ t.Fatalf("Should be unevaluated: %s", ak)
+ }
}
+ check(ab, false)
+ check(bb, true)
+ check(cb, true)
}
func TestDependency_Blocks_Unknown(t *testing.T) {
- state := NewStatus("a{host=ny01}")
- state.Touched = queryTime.Add(-10 * time.Minute)
- state.Append(&Event{Status: StNormal, Time: state.Touched})
-
+ defer setup()()
testSched(t, &schedTest{
conf: `alert a {
depends = avg(q("avg:b{host=*}", "5m", "")) > 0
@@ -255,24 +258,14 @@ func TestDependency_Blocks_Unknown(t *testing.T) {
},
},
state: map[schedState]bool{},
- previous: map[models.AlertKey]*State{
- "a{host=ny01}": state,
+ touched: map[models.AlertKey]time.Time{
+ "a{host=ny01}": queryTime.Add(-10 * time.Minute),
},
})
}
func TestDependency_AlertFunctionHasNoResults(t *testing.T) {
- pingState := NewStatus("a{host=ny01,source=bosun01}")
- pingState.Touched = queryTime.Add(-5 * time.Minute)
- pingState.Append(&Event{Status: StNormal, Time: pingState.Touched})
-
- scollState := NewStatus("b{host=ny01}")
- scollState.Touched = queryTime.Add(-10 * time.Minute)
- scollState.Append(&Event{Status: StNormal, Time: scollState.Touched})
-
- cpuState := NewStatus("c{host=ny01}")
- cpuState.Touched = queryTime.Add(-10 * time.Minute)
- cpuState.Append(&Event{Status: StWarning, Time: cpuState.Touched})
+ defer setup()()
testSched(t, &schedTest{
conf: `
@@ -304,10 +297,10 @@ alert c {
state: map[schedState]bool{
schedState{"a{host=ny01,source=bosun01}", "warning"}: true,
},
- previous: map[models.AlertKey]*State{
- "a{host=ny01,source=bosun01}": pingState,
- "b{host=ny01}": scollState,
- "c{host=ny01}": cpuState,
+ touched: map[models.AlertKey]time.Time{
+ "a{host=ny01,source=bosun01}": queryTime.Add(-5 * time.Minute),
+ "b{host=ny01}": queryTime.Add(-10 * time.Minute),
+ "c{host=ny01}": queryTime.Add(-10 * time.Minute),
},
})
}
diff --git a/cmd/bosun/sched/filter.go b/cmd/bosun/sched/filter.go
index 8e2dc48610..c53b7da088 100644
--- a/cmd/bosun/sched/filter.go
+++ b/cmd/bosun/sched/filter.go
@@ -5,16 +5,17 @@ import (
"strings"
"bosun.org/cmd/bosun/conf"
+ "bosun.org/models"
)
-func makeFilter(filter string) (func(*conf.Conf, *conf.Alert, *State) bool, error) {
+func makeFilter(filter string) (func(*conf.Conf, *conf.Alert, *models.IncidentState) bool, error) {
fields := strings.Fields(filter)
if len(fields) == 0 {
- return func(c *conf.Conf, a *conf.Alert, s *State) bool {
+ return func(c *conf.Conf, a *conf.Alert, s *models.IncidentState) bool {
return true
}, nil
}
- fs := make(map[string][]func(c *conf.Conf, a *conf.Alert, s *State) bool)
+ fs := make(map[string][]func(c *conf.Conf, a *conf.Alert, s *models.IncidentState) bool)
for _, f := range fields {
negate := strings.HasPrefix(f, "!")
if negate {
@@ -29,8 +30,8 @@ func makeFilter(filter string) (func(*conf.Conf, *conf.Alert, *State) bool, erro
if len(sp) == 1 {
key = ""
}
- add := func(fn func(c *conf.Conf, a *conf.Alert, s *State) bool) {
- fs[key] = append(fs[key], func(c *conf.Conf, a *conf.Alert, s *State) bool {
+ add := func(fn func(c *conf.Conf, a *conf.Alert, s *models.IncidentState) bool) {
+ fs[key] = append(fs[key], func(c *conf.Conf, a *conf.Alert, s *models.IncidentState) bool {
v := fn(c, a, s)
if negate {
v = !v
@@ -40,8 +41,8 @@ func makeFilter(filter string) (func(*conf.Conf, *conf.Alert, *State) bool, erro
}
switch key {
case "":
- add(func(c *conf.Conf, a *conf.Alert, s *State) bool {
- ak := s.AlertKey()
+ add(func(c *conf.Conf, a *conf.Alert, s *models.IncidentState) bool {
+ ak := s.AlertKey
return strings.Contains(string(ak), value) || strings.Contains(string(s.Subject), value)
})
case "ack":
@@ -54,14 +55,14 @@ func makeFilter(filter string) (func(*conf.Conf, *conf.Alert, *State) bool, erro
default:
return nil, fmt.Errorf("unknown %s value: %s", key, value)
}
- add(func(c *conf.Conf, a *conf.Alert, s *State) bool {
+ add(func(c *conf.Conf, a *conf.Alert, s *models.IncidentState) bool {
return s.NeedAck != v
})
case "notify":
- add(func(c *conf.Conf, a *conf.Alert, s *State) bool {
+ add(func(c *conf.Conf, a *conf.Alert, s *models.IncidentState) bool {
r := false
f := func(ns *conf.Notifications) {
- for k := range ns.Get(c, s.Group) {
+ for k := range ns.Get(c, s.AlertKey.Group()) {
if strings.Contains(k, value) {
r = true
break
@@ -73,27 +74,27 @@ func makeFilter(filter string) (func(*conf.Conf, *conf.Alert, *State) bool, erro
return r
})
case "status":
- var v Status
+ var v models.Status
switch value {
case "normal":
- v = StNormal
+ v = models.StNormal
case "warning":
- v = StWarning
+ v = models.StWarning
case "critical":
- v = StCritical
+ v = models.StCritical
case "unknown":
- v = StUnknown
+ v = models.StUnknown
default:
return nil, fmt.Errorf("unknown %s value: %s", key, value)
}
- add(func(c *conf.Conf, a *conf.Alert, s *State) bool {
- return s.AbnormalStatus() == v
+ add(func(c *conf.Conf, a *conf.Alert, s *models.IncidentState) bool {
+ return s.LastAbnormalStatus == v
})
default:
return nil, fmt.Errorf("unknown filter key: %s", key)
}
}
- return func(c *conf.Conf, a *conf.Alert, s *State) bool {
+ return func(c *conf.Conf, a *conf.Alert, s *models.IncidentState) bool {
for _, ors := range fs {
match := false
for _, f := range ors {
diff --git a/cmd/bosun/sched/grouping_test.go b/cmd/bosun/sched/grouping_test.go
index 92f625c245..05e886ef37 100644
--- a/cmd/bosun/sched/grouping_test.go
+++ b/cmd/bosun/sched/grouping_test.go
@@ -8,7 +8,8 @@ import (
)
func TestGroupSets_Single(t *testing.T) {
- states := States{"a{host=foo}": &State{Alert: "a", Group: opentsdb.TagSet{"host": "foo"}, Subject: "aaa"}}
+ ak := models.AlertKey("a{host=foo}")
+ states := States{ak: &models.IncidentState{AlertKey: ak, Alert: "a", Tags: opentsdb.TagSet{"host": "foo"}.Tags(), Subject: "aaa"}}
groups := states.GroupSets(5)
if len(groups) != 1 {
t.Fatalf("Expected 1 group. Found %d.", len(groups))
@@ -31,7 +32,7 @@ func TestGroupSets_AboveAndBelow(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- states[ak] = &State{Alert: ak.Name(), Group: ak.Group(), Subject: sub}
+ states[ak] = &models.IncidentState{AlertKey: models.AlertKey(a), Alert: ak.Name(), Tags: ak.Group().Tags(), Subject: sub}
}
groups := states.GroupSets(5)
@@ -58,7 +59,7 @@ func TestGroupSets_ByAlert(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- states[ak] = &State{Alert: ak.Name(), Group: ak.Group(), Subject: sub}
+ states[ak] = &models.IncidentState{AlertKey: models.AlertKey(a), Alert: ak.Name(), Tags: ak.Group().Tags(), Subject: sub}
}
groups := states.GroupSets(5)
diff --git a/cmd/bosun/sched/host.go b/cmd/bosun/sched/host.go
index 4cdafdd53d..8284a84e18 100644
--- a/cmd/bosun/sched/host.go
+++ b/cmd/bosun/sched/host.go
@@ -6,7 +6,6 @@ import (
"time"
"bosun.org/metadata"
- "bosun.org/models"
"bosun.org/opentsdb"
"bosun.org/slog"
)
@@ -21,7 +20,10 @@ func (s *Schedule) Host(filter string) (map[string]*HostData, error) {
for _, h := range allHosts {
hosts[h] = newHostData()
}
- states := s.GetOpenStates()
+ states, err := s.GetOpenStates()
+ if err != nil {
+ return nil, err
+ }
silences := s.Silenced()
// These are all fetched by metric since that is how we store it in redis,
// so this makes for the fastest response
@@ -614,24 +616,24 @@ func statusString(val, goodVal int64, goodName, badName string) string {
return badName
}
-func processHostIncidents(host *HostData, states States, silences map[models.AlertKey]models.Silence) {
+func processHostIncidents(host *HostData, states States, silences SilenceTester) {
for ak, state := range states {
- if stateHost, ok := state.Group["host"]; !ok {
+ if stateHost, ok := state.AlertKey.Group()["host"]; !ok {
continue
} else if stateHost != host.Name {
continue
}
- _, silenced := silences[ak]
+ silenced := silences(ak)
is := IncidentStatus{
- IncidentID: state.Last().IncidentId,
+ IncidentID: state.Id,
Active: state.IsActive(),
- AlertKey: state.AlertKey(),
- Status: state.Status(),
+ AlertKey: state.AlertKey,
+ Status: state.CurrentStatus,
StatusTime: state.Last().Time.Unix(),
Subject: state.Subject,
- Silenced: silenced,
- LastAbnormalStatus: state.AbnormalStatus(),
- LastAbnormalTime: state.AbnormalEvent().Time.Unix(),
+ Silenced: silenced != nil,
+ LastAbnormalStatus: state.LastAbnormalStatus,
+ LastAbnormalTime: state.LastAbnormalTime,
NeedsAck: state.NeedAck,
}
host.OpenIncidents = append(host.OpenIncidents, is)
diff --git a/cmd/bosun/sched/notification_test.go b/cmd/bosun/sched/notification_test.go
index 086344a51d..0cf70e2610 100644
--- a/cmd/bosun/sched/notification_test.go
+++ b/cmd/bosun/sched/notification_test.go
@@ -7,7 +7,6 @@ import (
"bosun.org/cmd/bosun/conf"
"bosun.org/models"
- "bosun.org/opentsdb"
)
func TestActionNotificationTemplates(t *testing.T) {
@@ -18,15 +17,15 @@ func TestActionNotificationTemplates(t *testing.T) {
}
s, _ := initSched(c)
data := &actionNotificationContext{}
- data.ActionType = ActionAcknowledge
+ data.ActionType = models.ActionAcknowledge
data.Message = "Bad things happened"
data.User = "Batman"
- data.States = []*State{
+ data.States = []*models.IncidentState{
{
- History: []Event{
+ Id: 224,
+ Events: []models.Event{
{
- Status: StCritical,
- IncidentId: 224,
+ Status: models.StCritical,
},
},
Alert: "xyz",
@@ -53,6 +52,7 @@ func TestActionNotificationTemplates(t *testing.T) {
}
func TestActionNotificationGrouping(t *testing.T) {
+ defer setup()()
c, err := conf.New("", `
template t{
subject = 2
@@ -114,14 +114,18 @@ func TestActionNotificationGrouping(t *testing.T) {
bcrit := models.AlertKey("b{host=c}")
cA := models.AlertKey("c{host=a}")
cB := models.AlertKey("c{host=b}")
- s.status[awarn] = &State{Alert: "a", Group: opentsdb.TagSet{"host": "w"}, History: []Event{{Status: StWarning}}}
- s.status[acrit] = &State{Alert: "a", Group: opentsdb.TagSet{"host": "c"}, History: []Event{{Status: StCritical}}}
- s.status[bwarn] = &State{Alert: "b", Group: opentsdb.TagSet{"host": "w"}, History: []Event{{Status: StWarning}}}
- s.status[bcrit] = &State{Alert: "b", Group: opentsdb.TagSet{"host": "c"}, History: []Event{{Status: StCritical}}}
- s.status[cA] = &State{Alert: "c", Group: opentsdb.TagSet{"host": "a"}, History: []Event{{Status: StWarning}}}
- s.status[cB] = &State{Alert: "c", Group: opentsdb.TagSet{"host": "b"}, History: []Event{{Status: StWarning}}}
+ da := s.DataAccess.State()
+ da.UpdateIncidentState(&models.IncidentState{AlertKey: awarn, Alert: awarn.Name(), Tags: awarn.Group().Tags(), WorstStatus: models.StWarning, Events: []models.Event{{Status: models.StWarning}}})
+ da.UpdateIncidentState(&models.IncidentState{AlertKey: acrit, Alert: acrit.Name(), Tags: acrit.Group().Tags(), WorstStatus: models.StCritical, Events: []models.Event{{Status: models.StCritical}}})
+ da.UpdateIncidentState(&models.IncidentState{AlertKey: bwarn, Alert: bwarn.Name(), Tags: bwarn.Group().Tags(), WorstStatus: models.StWarning, Events: []models.Event{{Status: models.StWarning}}})
+ da.UpdateIncidentState(&models.IncidentState{AlertKey: bcrit, Alert: bcrit.Name(), Tags: bcrit.Group().Tags(), WorstStatus: models.StCritical, Events: []models.Event{{Status: models.StCritical}}})
+ da.UpdateIncidentState(&models.IncidentState{AlertKey: cA, Alert: cA.Name(), Tags: cA.Group().Tags(), WorstStatus: models.StWarning, Events: []models.Event{{Status: models.StWarning}}})
+ da.UpdateIncidentState(&models.IncidentState{AlertKey: cB, Alert: cB.Name(), Tags: cB.Group().Tags(), WorstStatus: models.StWarning, Events: []models.Event{{Status: models.StWarning}}})
- groups := s.groupActionNotifications([]models.AlertKey{awarn, acrit, bwarn, bcrit, cA, cB})
+ groups, err := s.groupActionNotifications([]models.AlertKey{awarn, acrit, bwarn, bcrit, cA, cB})
+ if err != nil {
+ t.Fatal(err)
+ }
expect := func(not string, aks ...models.AlertKey) {
n := c.Notifications[not]
actualAks, ok := groups[n]
@@ -132,8 +136,8 @@ func TestActionNotificationGrouping(t *testing.T) {
t.Fatalf("Count mismatch for grouping %s. %d != %d.", not, len(actualAks), len(aks))
}
for i, ak := range aks {
- if actualAks[i].AlertKey() != ak {
- t.Fatalf("Alert key mismatch at index %d. %s != %s.", i, actualAks[i].AlertKey(), ak)
+ if actualAks[i].AlertKey != ak {
+ t.Fatalf("Alert key mismatch at index %d. %s != %s.", i, actualAks[i].AlertKey, ak)
}
}
}
diff --git a/cmd/bosun/sched/notify.go b/cmd/bosun/sched/notify.go
index 9a0b330fb7..5107be0b3f 100644
--- a/cmd/bosun/sched/notify.go
+++ b/cmd/bosun/sched/notify.go
@@ -29,9 +29,9 @@ func (s *Schedule) dispatchNotifications() {
}
-func (s *Schedule) Notify(st *State, n *conf.Notification) {
+func (s *Schedule) Notify(st *models.IncidentState, n *conf.Notification) {
if s.pendingNotifications == nil {
- s.pendingNotifications = make(map[*conf.Notification][]*State)
+ s.pendingNotifications = make(map[*conf.Notification][]*models.IncidentState)
}
s.pendingNotifications[n] = append(s.pendingNotifications[n], st)
}
@@ -45,7 +45,7 @@ func (s *Schedule) CheckNotifications() time.Duration {
notifications := s.Notifications
s.Notifications = nil
for ak, ns := range notifications {
- if _, present := silenced[ak]; present {
+ if si := silenced(ak); si != nil {
slog.Infoln("silencing", ak)
continue
}
@@ -59,16 +59,30 @@ func (s *Schedule) CheckNotifications() time.Duration {
s.AddNotification(ak, n, t)
continue
}
- st := s.status[ak]
- if st == nil {
- continue
+
+ //If alert is currently unevaluated because of a dependency,
+ //simply requeue it until the dependency resolves itself.
+ _, uneval := s.GetUnknownAndUnevaluatedAlertKeys(ak.Name())
+ unevaluated := false
+ for _, un := range uneval {
+ if un == ak {
+ unevaluated = true
+ break
+ }
}
- // If alert is currently unevaluated because of a dependency,
- // simply requeue it until the dependency resolves itself.
- if st.Unevaluated {
+ if unevaluated {
s.AddNotification(ak, n, t)
continue
}
+ st, err := s.DataAccess.State().GetLatestIncident(ak)
+ if err != nil {
+ slog.Error(err)
+ continue
+ }
+ if st == nil {
+ continue
+ }
+
s.Notify(st, n)
}
}
@@ -91,16 +105,16 @@ func (s *Schedule) CheckNotifications() time.Duration {
return timeout
}
-func (s *Schedule) sendNotifications(silenced map[models.AlertKey]models.Silence) {
+func (s *Schedule) sendNotifications(silenced SilenceTester) {
if s.Conf.Quiet {
slog.Infoln("quiet mode prevented", len(s.pendingNotifications), "notifications")
return
}
for n, states := range s.pendingNotifications {
for _, st := range states {
- ak := st.AlertKey()
- _, silenced := silenced[ak]
- if st.Last().Status == StUnknown {
+ ak := st.AlertKey
+ silenced := silenced(ak) != nil
+ if st.CurrentStatus == models.StUnknown {
if silenced {
slog.Infoln("silencing unknown", ak)
continue
@@ -124,7 +138,7 @@ func (s *Schedule) sendUnknownNotifications() {
for n, states := range s.pendingUnknowns {
ustates := make(States)
for _, st := range states {
- ustates[st.AlertKey()] = st
+ ustates[st.AlertKey] = st
}
var c int
tHit := false
@@ -148,7 +162,7 @@ func (s *Schedule) sendUnknownNotifications() {
s.utnotify(oTSets, n)
}
}
- s.pendingUnknowns = make(map[*conf.Notification][]*State)
+ s.pendingUnknowns = make(map[*conf.Notification][]*models.IncidentState)
}
var unknownMultiGroup = ttemplate.Must(ttemplate.New("unknownMultiGroup").Parse(`
@@ -168,8 +182,8 @@ var unknownMultiGroup = ttemplate.Must(ttemplate.New("unknownMultiGroup").Parse(
`))
-func (s *Schedule) notify(st *State, n *conf.Notification) {
- n.Notify(st.Subject, st.Body, st.EmailSubject, st.EmailBody, s.Conf, string(st.AlertKey()), st.Attachments...)
+func (s *Schedule) notify(st *models.IncidentState, n *conf.Notification) {
+ n.Notify(st.Subject, st.Body, st.EmailSubject, st.EmailBody, s.Conf, string(st.AlertKey), st.Attachments...)
}
// utnotify is single notification for N unknown groups into a single notification
@@ -247,7 +261,7 @@ func init() {
subject := `{{$first := index .States 0}}{{$count := len .States}}
{{.User}} {{.ActionType}}
{{if gt $count 1}} {{$count}} Alerts.
-{{else}} Incident #{{$first.Last.IncidentId}} ({{$first.Subject}})
+{{else}} Incident #{{$first.Id}} ({{$first.Subject}})
{{end}}`
body := `{{$count := len .States}}{{.User}} {{.ActionType}} {{$count}} alert{{if gt $count 1}}s{{end}}:
Message: {{.Message}}
@@ -255,7 +269,7 @@ func init() {
{{range .States}}
-
- #{{.Last.IncidentId}}:
+ #{{.Id}}:
{{.Subject}}
{{end}}
@@ -264,11 +278,13 @@ func init() {
actionNotificationBodyTemplate = htemplate.Must(htemplate.New("").Parse(body))
}
-func (s *Schedule) ActionNotify(at ActionType, user, message string, aks []models.AlertKey) {
- groupings := s.groupActionNotifications(aks)
-
+func (s *Schedule) ActionNotify(at models.ActionType, user, message string, aks []models.AlertKey) error {
+ groupings, err := s.groupActionNotifications(aks)
+ if err != nil {
+ return err
+ }
for notification, states := range groupings {
- incidents := []*State{}
+ incidents := []*models.IncidentState{}
for _, state := range states {
incidents = append(incidents, state)
}
@@ -289,18 +305,22 @@ func (s *Schedule) ActionNotify(at ActionType, user, message string, aks []model
notification.Notify(subject, buf.String(), []byte(subject), buf.Bytes(), s.Conf, "actionNotification")
}
+ return nil
}
-func (s *Schedule) groupActionNotifications(aks []models.AlertKey) map[*conf.Notification][]*State {
- groupings := make(map[*conf.Notification][]*State)
+func (s *Schedule) groupActionNotifications(aks []models.AlertKey) (map[*conf.Notification][]*models.IncidentState, error) {
+ groupings := make(map[*conf.Notification][]*models.IncidentState)
for _, ak := range aks {
alert := s.Conf.Alerts[ak.Name()]
- status := s.GetStatus(ak)
+ status, err := s.DataAccess.State().GetLatestIncident(ak)
+ if err != nil {
+ return nil, err
+ }
if alert == nil || status == nil {
continue
}
var n *conf.Notifications
- if status.Status() == StWarning {
+ if status.WorstStatus == models.StWarning || alert.CritNotification == nil {
n = alert.WarnNotification
} else {
n = alert.CritNotification
@@ -316,5 +336,5 @@ func (s *Schedule) groupActionNotifications(aks []models.AlertKey) map[*conf.Not
groupings[not] = append(groupings[not], status)
}
}
- return groupings
+ return groupings, nil
}
diff --git a/cmd/bosun/sched/sched.go b/cmd/bosun/sched/sched.go
index 3a11d7bec4..4d9b30a411 100644
--- a/cmd/bosun/sched/sched.go
+++ b/cmd/bosun/sched/sched.go
@@ -2,7 +2,6 @@ package sched // import "bosun.org/cmd/bosun/sched"
import (
"encoding/gob"
- "encoding/json"
"fmt"
"net"
"reflect"
@@ -37,24 +36,24 @@ type Schedule struct {
mutexAquired time.Time
mutexWaitTime int64
- Conf *conf.Conf
- status States
- Group map[time.Time]models.AlertKeys
+ Conf *conf.Conf
+ Group map[time.Time]models.AlertKeys
Search *search.Search
//channel signals an alert has added notifications, and notifications should be processed.
nc chan interface{}
//notifications to be sent immediately
- pendingNotifications map[*conf.Notification][]*State
+ pendingNotifications map[*conf.Notification][]*models.IncidentState
//notifications we are currently tracking, potentially with future or repeated actions.
Notifications map[models.AlertKey]map[string]time.Time
//unknown states that need to be notified about. Collected and sent in batches.
- pendingUnknowns map[*conf.Notification][]*State
+ pendingUnknowns map[*conf.Notification][]*models.IncidentState
db *bolt.DB
- LastCheck time.Time
+ lastLogTimes map[models.AlertKey]time.Time
+ LastCheck time.Time
ctx *checkContext
@@ -69,8 +68,8 @@ func (s *Schedule) Init(c *conf.Conf) error {
var err error
s.Conf = c
s.Group = make(map[time.Time]models.AlertKeys)
- s.pendingUnknowns = make(map[*conf.Notification][]*State)
- s.status = make(States)
+ s.pendingUnknowns = make(map[*conf.Notification][]*models.IncidentState)
+ s.lastLogTimes = make(map[models.AlertKey]time.Time)
s.LastCheck = time.Now()
s.ctx = &checkContext{time.Now(), cache.New(0)}
if s.DataAccess == nil {
@@ -219,26 +218,26 @@ func (s *Schedule) GetMetadata(metric string, subset opentsdb.TagSet) ([]metadat
return ms, nil
}
-type States map[models.AlertKey]*State
+type States map[models.AlertKey]*models.IncidentState
type StateTuple struct {
NeedAck bool
Active bool
- Status Status
- CurrentStatus Status
+ Status models.Status
+ CurrentStatus models.Status
Silenced bool
}
// GroupStates groups by NeedAck, Active, Status, and Silenced.
-func (states States) GroupStates(silenced map[models.AlertKey]models.Silence) map[StateTuple]States {
+func (states States) GroupStates(silenced SilenceTester) map[StateTuple]States {
r := make(map[StateTuple]States)
for ak, st := range states {
- _, sil := silenced[ak]
+ sil := silenced(ak) != nil
t := StateTuple{
NeedAck: st.NeedAck,
Active: st.IsActive(),
- Status: st.AbnormalStatus(),
- CurrentStatus: st.Status(),
+ Status: st.LastAbnormalStatus,
+ CurrentStatus: st.CurrentStatus,
Silenced: sil,
}
if _, present := r[t]; !present {
@@ -256,14 +255,14 @@ func (states States) GroupSets(minGroup int) map[string]models.AlertKeys {
k, v string
}
groups := make(map[string]models.AlertKeys)
- seen := make(map[*State]bool)
+ seen := make(map[*models.IncidentState]bool)
for {
counts := make(map[Pair]int)
for _, s := range states {
if seen[s] {
continue
}
- for k, v := range s.Group {
+ for k, v := range s.AlertKey.Group() {
counts[Pair{k, v}]++
}
}
@@ -286,11 +285,11 @@ func (states States) GroupSets(minGroup int) map[string]models.AlertKeys {
if seen[s] {
continue
}
- if s.Group[pair.k] != pair.v {
+ if s.AlertKey.Group()[pair.k] != pair.v {
continue
}
seen[s] = true
- group = append(group, s.AlertKey())
+ group = append(group, s.AlertKey)
}
if len(group) > 0 {
groups[fmt.Sprintf("{%s=%s}", pair.k, pair.v)] = group
@@ -302,7 +301,7 @@ func (states States) GroupSets(minGroup int) map[string]models.AlertKeys {
if seen[s] {
continue
}
- groupedByAlert[s.Alert] = append(groupedByAlert[s.Alert], s.AlertKey())
+ groupedByAlert[s.Alert] = append(groupedByAlert[s.Alert], s.AlertKey)
}
for a, aks := range groupedByAlert {
if len(aks) >= minGroup {
@@ -318,43 +317,35 @@ func (states States) GroupSets(minGroup int) map[string]models.AlertKeys {
if seen[s] || len(groupedByAlert[s.Alert]) >= minGroup {
continue
}
- groups[string(s.AlertKey())] = models.AlertKeys{s.AlertKey()}
+ groups[string(s.AlertKey)] = models.AlertKeys{s.AlertKey}
}
return groups
}
-func (states States) Copy() States {
- newStates := make(States, len(states))
- for ak, st := range states {
- newStates[ak] = st.Copy()
+func (s *Schedule) GetOpenStates() (States, error) {
+ incidents, err := s.DataAccess.State().GetAllOpenIncidents()
+ if err != nil {
+ return nil, err
}
- return newStates
-}
-
-func (s *Schedule) GetOpenStates() States {
- s.Lock("GetOpenStates")
- defer s.Unlock()
- states := s.status.Copy()
- for k, state := range states {
- if !state.Open {
- delete(states, k)
- }
+ states := make(States, len(incidents))
+ for _, inc := range incidents {
+ states[inc.AlertKey] = inc
}
- return states
+ return states, nil
}
type StateGroup struct {
Active bool `json:",omitempty"`
- Status Status
- CurrentStatus Status
+ Status models.Status
+ CurrentStatus models.Status
Silenced bool
- IsError bool `json:",omitempty"`
- Subject string `json:",omitempty"`
- Alert string `json:",omitempty"`
- AlertKey models.AlertKey `json:",omitempty"`
- Ago string `json:",omitempty"`
- State *State `json:",omitempty"`
- Children []*StateGroup `json:",omitempty"`
+ IsError bool `json:",omitempty"`
+ Subject string `json:",omitempty"`
+ Alert string `json:",omitempty"`
+ AlertKey models.AlertKey `json:",omitempty"`
+ Ago string `json:",omitempty"`
+ State *models.IncidentState `json:",omitempty"`
+ Children []*StateGroup `json:",omitempty"`
}
type StateGroups struct {
@@ -367,7 +358,7 @@ type StateGroups struct {
}
func (s *Schedule) MarshalGroups(T miniprofiler.Timer, filter string) (*StateGroups, error) {
- var silenced map[models.AlertKey]models.Silence
+ var silenced SilenceTester
T.Step("Silenced", func(miniprofiler.Timer) {
silenced = s.Silenced()
})
@@ -378,28 +369,27 @@ func (s *Schedule) MarshalGroups(T miniprofiler.Timer, filter string) (*StateGro
TimeAndDate: s.Conf.TimeAndDate,
}
t.FailingAlerts, t.UnclosedErrors = s.getErrorCounts()
- s.Lock("MarshallGroups")
- defer s.Unlock()
T.Step("Setup", func(miniprofiler.Timer) {
matches, err2 := makeFilter(filter)
if err2 != nil {
err = err2
return
}
- for k, v := range s.status {
- if !v.Open {
- continue
- }
+ status2, err2 := s.GetOpenStates()
+ if err2 != nil {
+ err = err2
+ return
+ }
+ for k, v := range status2 {
a := s.Conf.Alerts[k.Name()]
if a == nil {
- err = fmt.Errorf("unknown alert %s", k.Name())
- return
+ slog.Errorf("unknown alert %s", k.Name())
+ continue
}
if matches(s.Conf, a, v) {
status[k] = v
}
}
-
})
if err != nil {
return nil, err
@@ -411,7 +401,7 @@ func (s *Schedule) MarshalGroups(T miniprofiler.Timer, filter string) (*StateGro
for tuple, states := range groups {
var grouped []*StateGroup
switch tuple.Status {
- case StWarning, StCritical, StUnknown:
+ case models.StWarning, models.StCritical, models.StUnknown:
var sets map[string]models.AlertKeys
T.Step(fmt.Sprintf("GroupSets (%d): %v", len(states), tuple), func(T miniprofiler.Timer) {
sets = states.GroupSets(s.Conf.MinGroupSize)
@@ -425,17 +415,9 @@ func (s *Schedule) MarshalGroups(T miniprofiler.Timer, filter string) (*StateGro
Subject: fmt.Sprintf("%s - %s", tuple.Status, name),
}
for _, ak := range group {
- st := s.status[ak].Copy()
- // remove some of the larger bits of state to reduce wire size
+ st := status[ak]
st.Body = ""
- st.EmailBody = []byte{}
- if len(st.History) > 1 {
- st.History = st.History[len(st.History)-1:]
- }
- if len(st.Actions) > 1 {
- st.Actions = st.Actions[len(st.Actions)-1:]
- }
-
+ st.EmailBody = nil
g.Children = append(g.Children, &StateGroup{
Active: tuple.Active,
Status: tuple.Status,
@@ -592,112 +574,14 @@ func init() {
"The running count of actions performed by individual users (Closed alert, Acknowledged alert, etc).")
}
-type State struct {
- *Result
-
- // Most recent last.
- History []Event `json:",omitempty"`
- Actions []Action `json:",omitempty"`
- Touched time.Time
- Alert string // helper data since AlertKeys don't serialize to JSON well
- Tags string // string representation of Group
- Group opentsdb.TagSet
- Subject string
- Body string
- EmailBody []byte `json:"-"`
- EmailSubject []byte `json:"-"`
- Attachments []*conf.Attachment `json:"-"`
- NeedAck bool
- Open bool
- Forgotten bool
- Unevaluated bool
- LastLogTime time.Time
-}
-
-func (s *State) Copy() *State {
- newState := &State{
- History: s.History, //history and actions safe to copy as long as elements are not modified. Appending will not affect original state.
- Actions: s.Actions,
- Touched: s.Touched,
- Alert: s.Alert,
- Tags: s.Tags,
- Group: s.Group.Copy(),
- Subject: s.Subject,
- Body: s.Body,
- EmailBody: s.EmailBody,
- EmailSubject: s.EmailSubject,
- Attachments: s.Attachments,
- NeedAck: s.NeedAck,
- Open: s.Open,
- Forgotten: s.Forgotten,
- Unevaluated: s.Unevaluated,
- LastLogTime: s.LastLogTime,
- }
- newState.Result = s.Result
- return newState
-}
-
-func (s *State) AlertKey() models.AlertKey {
- return models.NewAlertKey(s.Alert, s.Group)
-}
-
-func (s *State) Status() Status {
- return s.Last().Status
-}
-
-// AbnormalEvent returns the most recent non-normal event, or nil if none found.
-func (s *State) AbnormalEvent() *Event {
- for i := len(s.History) - 1; i >= 0; i-- {
- if ev := s.History[i]; ev.Status > StNormal {
- return &ev
- }
- }
- return nil
-}
-
-// AbnormalStatus returns the most recent non-normal status, or StNone if none
-// found.
-func (s *State) AbnormalStatus() Status {
- ev := s.AbnormalEvent()
- if ev != nil {
- return ev.Status
+func (s *Schedule) Action(user, message string, t models.ActionType, ak models.AlertKey) error {
+ if err := collect.Add("actions", opentsdb.TagSet{"user": user, "alert": ak.Name(), "type": t.String()}, 1); err != nil {
+ slog.Errorln(err)
}
- return StNone
-}
-
-// WorstThisIncident returns the highest severity event with the same IncidentId as the Last event.
-func (s *State) WorstThisIncident() Status {
- ev := s.Last()
- worst := ev.Status
- for i := len(s.History) - 2; i >= 0; i-- {
- ev2 := s.History[i]
- if ev2.IncidentId != ev.IncidentId {
- break
- }
- if ev2.Status > worst {
- worst = ev2.Status
- }
+ st, err := s.DataAccess.State().GetLatestIncident(ak)
+ if err != nil {
+ return err
}
- return worst
-}
-
-func (s *State) IsActive() bool {
- return s.Status() > StNormal
-}
-
-func (s *State) Action(user, message string, t ActionType, timestamp time.Time) {
- s.Actions = append(s.Actions, Action{
- User: user,
- Message: message,
- Type: t,
- Time: timestamp,
- })
-}
-
-func (s *Schedule) Action(user, message string, t ActionType, ak models.AlertKey) error {
- s.Lock("Action")
- defer s.Unlock()
- st := s.status[ak]
if st == nil {
return fmt.Errorf("no such alert key: %v", ak)
}
@@ -705,10 +589,10 @@ func (s *Schedule) Action(user, message string, t ActionType, ak models.AlertKey
delete(s.Notifications, ak)
st.NeedAck = false
}
- isUnknown := st.AbnormalStatus() == StUnknown
+ isUnknown := st.LastAbnormalStatus == models.StUnknown
timestamp := time.Now().UTC()
switch t {
- case ActionAcknowledge:
+ case models.ActionAcknowledge:
if !st.NeedAck {
return fmt.Errorf("alert already acknowledged")
}
@@ -716,7 +600,7 @@ func (s *Schedule) Action(user, message string, t ActionType, ak models.AlertKey
return fmt.Errorf("cannot acknowledge closed alert")
}
ack()
- case ActionClose:
+ case models.ActionClose:
if st.NeedAck {
ack()
}
@@ -724,238 +608,38 @@ func (s *Schedule) Action(user, message string, t ActionType, ak models.AlertKey
return fmt.Errorf("cannot close active alert")
}
st.Open = false
- last := st.Last()
- if last.IncidentId != 0 {
- incident, err := s.DataAccess.Incidents().GetIncident(last.IncidentId)
- if err != nil {
- return err
- }
- incident.End = ×tamp
- if err = s.DataAccess.Incidents().UpdateIncident(last.IncidentId, incident); err != nil {
- return err
- }
-
- }
- case ActionForget:
+ st.End = ×tamp
+ case models.ActionForget:
if !isUnknown {
return fmt.Errorf("can only forget unknowns")
}
- if st.NeedAck {
- ack()
- }
- st.Open = false
- st.Forgotten = true
- delete(s.status, ak)
+ return s.DataAccess.State().Forget(ak)
default:
return fmt.Errorf("unknown action type: %v", t)
}
- st.Action(user, message, t, timestamp)
// Would like to also track the alert group, but I believe this is impossible because any character
// that could be used as a delimiter could also be a valid tag key or tag value character
if err := collect.Add("actions", opentsdb.TagSet{"user": user, "alert": ak.Name(), "type": t.String()}, 1); err != nil {
slog.Errorln(err)
}
- return nil
-}
-
-func (s *State) Touch() {
- s.Touched = time.Now().UTC()
- s.Forgotten = false
-}
-
-// Append appends status to the history if the status is different than the
-// latest status. Returns the previous status.
-func (s *State) Append(event *Event) Status {
- last := s.Last()
- if len(s.History) == 0 || last.Status != event.Status {
- s.History = append(s.History, *event)
- }
- return last.Status
-}
-
-func (s *State) Last() Event {
- if len(s.History) == 0 {
- return Event{}
- }
- return s.History[len(s.History)-1]
-}
-
-type Event struct {
- Warn, Crit *Result
- Status Status
- Time time.Time
- Unevaluated bool
- IncidentId uint64
-}
-
-type Result struct {
- *expr.Result
- Expr string
-}
-
-func (r *Result) Copy() *Result {
- return &Result{r.Result, r.Expr}
-}
-
-type Status int
-
-const (
- StNone Status = iota
- StNormal
- StWarning
- StCritical
- StUnknown
-)
-
-func (s Status) String() string {
- switch s {
- case StNormal:
- return "normal"
- case StWarning:
- return "warning"
- case StCritical:
- return "critical"
- case StUnknown:
- return "unknown"
- default:
- return "none"
- }
-}
-
-func (s Status) MarshalJSON() ([]byte, error) {
- return json.Marshal(s.String())
-}
-
-func (s *Status) UnmarshalJSON(b []byte) error {
- switch string(b) {
- case "normal":
- *s = StNormal
- case "warning":
- *s = StWarning
- case "critical":
- *s = StCritical
- case "unknown":
- *s = StUnknown
- default:
- *s = StNone
- }
- return nil
-}
-
-func (s Status) IsNormal() bool { return s == StNormal }
-func (s Status) IsWarning() bool { return s == StWarning }
-func (s Status) IsCritical() bool { return s == StCritical }
-func (s Status) IsUnknown() bool { return s == StUnknown }
-
-type Action struct {
- User string
- Message string
- Time time.Time
- Type ActionType
-}
-
-type ActionType int
-
-const (
- ActionNone ActionType = iota
- ActionAcknowledge
- ActionClose
- ActionForget
-)
-
-func (a ActionType) String() string {
- switch a {
- case ActionAcknowledge:
- return "Acknowledged"
- case ActionClose:
- return "Closed"
- case ActionForget:
- return "Forgotten"
- default:
- return "none"
- }
-}
-
-func (a ActionType) MarshalJSON() ([]byte, error) {
- return json.Marshal(a.String())
-}
-
-func (s *Schedule) createIncident(ak models.AlertKey, start time.Time) (*models.Incident, error) {
- return s.DataAccess.Incidents().CreateIncident(ak, start)
-}
-
-type incidentList []*models.Incident
-
-func (i incidentList) Len() int { return len(i) }
-func (i incidentList) Less(a int, b int) bool {
- if i[a].Start.Before(i[b].Start) {
- return true
- }
- return i[a].AlertKey < i[b].AlertKey
-}
-func (i incidentList) Swap(a int, b int) { i[a], i[b] = i[b], i[a] }
-
-func (s *Schedule) GetIncidents(alert string, from, to time.Time) ([]*models.Incident, error) {
-
- list, err := s.DataAccess.Incidents().GetIncidentsStartingInRange(from, to)
- if err != nil {
- return nil, err
- }
- incidents := []*models.Incident{}
- for _, i := range list {
- if alert != "" && i.AlertKey.Name() != alert {
- continue
- }
- incidents = append(incidents, i)
- }
- return incidents, nil
-}
-
-func (s *Schedule) GetIncident(id uint64) (*models.Incident, error) {
- i, err := s.DataAccess.Incidents().GetIncident(id)
- if err != nil {
- return nil, err
- }
- return i, nil
-}
-
-func (s *Schedule) GetIncidentEvents(id uint64) (*models.Incident, []Event, []Action, error) {
- incident, err := s.DataAccess.Incidents().GetIncident(id)
- if err != nil {
- return nil, nil, nil, err
- }
- list := []Event{}
- state := s.GetStatus(incident.AlertKey)
- if state == nil {
- return incident, list, nil, nil
- }
- found := false
- for _, e := range state.History {
- if e.IncidentId == id {
- found = true
- list = append(list, e)
- } else if found {
- break
- }
- }
- actions := []Action{}
- for _, a := range state.Actions {
- if a.Time.After(incident.Start) && (incident.End == nil || a.Time.Before(*incident.End) || a.Time.Equal(*incident.End)) {
- actions = append(actions, a)
- }
- }
- return incident, list, actions, nil
+ st.Actions = append(st.Actions, models.Action{
+ Message: message,
+ Time: timestamp,
+ Type: t,
+ User: user,
+ })
+ return s.DataAccess.State().UpdateIncidentState(st)
}
type IncidentStatus struct {
- IncidentID uint64
+ IncidentID int64
Active bool
AlertKey models.AlertKey
- Status Status
+ Status models.Status
StatusTime int64
Subject string
Silenced bool
- LastAbnormalStatus Status
+ LastAbnormalStatus models.Status
LastAbnormalTime int64
NeedsAck bool
}
diff --git a/cmd/bosun/sched/sched_test.go b/cmd/bosun/sched/sched_test.go
index 00f25a92ed..6386098b3e 100644
--- a/cmd/bosun/sched/sched_test.go
+++ b/cmd/bosun/sched/sched_test.go
@@ -15,6 +15,7 @@ import (
"bosun.org/_third_party/github.com/MiniProfiler/go/miniprofiler"
"bosun.org/cmd/bosun/conf"
"bosun.org/cmd/bosun/database"
+ "bosun.org/cmd/bosun/database/test"
"bosun.org/models"
"bosun.org/opentsdb"
"bosun.org/slog"
@@ -33,8 +34,8 @@ type schedTest struct {
conf string
queries map[string]opentsdb.ResponseSet
// state -> active
- state map[schedState]bool
- previous map[models.AlertKey]*State
+ state map[schedState]bool
+ touched map[models.AlertKey]time.Time
}
// test-only function to check all alerts immediately.
@@ -51,77 +52,18 @@ func check(s *Schedule, t time.Time) {
}
}
-//fake data access for tests. Perhaps a full mock would be more appropriate, once the interface contains more.
-//any methods not explicitely implemented will likely cause a nil reference panic. This is good.
-type nopDataAccess struct {
- database.MetadataDataAccess
- database.SearchDataAccess
- database.ErrorDataAccess
- database.IncidentDataAccess
- database.SilenceDataAccess
- failingAlerts map[string]bool
- idCounter uint64
- incidents map[uint64]*models.Incident
- silences map[string]*models.Silence
-}
-
-func (n *nopDataAccess) Search() database.SearchDataAccess { return n }
-func (n *nopDataAccess) Metadata() database.MetadataDataAccess { return n }
-func (n *nopDataAccess) Errors() database.ErrorDataAccess { return n }
-func (n *nopDataAccess) Incidents() database.IncidentDataAccess { return n }
-func (n *nopDataAccess) Silence() database.SilenceDataAccess { return n }
-
-func (n *nopDataAccess) BackupLastInfos(map[string]map[string]*database.LastInfo) error { return nil }
-func (n *nopDataAccess) LoadLastInfos() (map[string]map[string]*database.LastInfo, error) {
- return map[string]map[string]*database.LastInfo{}, nil
-}
-func (n *nopDataAccess) MarkAlertSuccess(name string) error {
- n.failingAlerts[name] = false
- return nil
-}
-func (n *nopDataAccess) MarkAlertFailure(name string, msg string) error {
- n.failingAlerts[name] = true
- return nil
-}
-func (n *nopDataAccess) GetFailingAlertCounts() (int, int, error) { return 0, 0, nil }
-func (n *nopDataAccess) IsAlertFailing(name string) (bool, error) { return n.failingAlerts[name], nil }
+var db database.DataAccess
-func (n *nopDataAccess) CreateIncident(ak models.AlertKey, start time.Time) (*models.Incident, error) {
- n.idCounter++
- n.incidents[n.idCounter] = &models.Incident{Id: n.idCounter, Start: start, AlertKey: ak}
- return n.incidents[n.idCounter], nil
-}
-func (n *nopDataAccess) GetIncident(id uint64) (*models.Incident, error) {
- return n.incidents[id], nil
-}
-func (n *nopDataAccess) UpdateIncident(id uint64, i *models.Incident) error {
- n.incidents[id] = i
- return nil
-}
-func (n *nopDataAccess) GetActiveSilences() ([]*models.Silence, error) {
- r := make([]*models.Silence, 0, len(n.silences))
- for _, s := range n.silences {
- r = append(r, s)
- }
- return r, nil
-}
-func (n *nopDataAccess) DeleteSilence(id string) error {
- delete(n.silences, id)
- return nil
-}
-func (n *nopDataAccess) AddSilence(s *models.Silence) error {
- n.silences[s.ID()] = s
- return nil
+func setup() func() {
+ testDb, closer := dbtest.StartTestRedis(9992)
+ db = testDb
+ return closer
}
func initSched(c *conf.Conf) (*Schedule, error) {
c.StateFile = ""
s := new(Schedule)
- s.DataAccess = &nopDataAccess{
- failingAlerts: map[string]bool{},
- incidents: map[uint64]*models.Incident{},
- silences: map[string]*models.Silence{},
- }
+ s.DataAccess = db
err := s.Init(c)
return s, err
}
@@ -165,8 +107,8 @@ func testSched(t *testing.T, st *schedTest) (s *Schedule) {
time.Sleep(time.Millisecond * 250)
s, _ = initSched(c)
- if st.previous != nil {
- s.status = st.previous
+ for ak, time := range st.touched {
+ s.DataAccess.State().TouchAlertKey(ak, time)
}
check(s, queryTime)
groups, err := s.MarshalGroups(new(miniprofiler.Profile), "")
@@ -210,6 +152,7 @@ var queryTime = time.Date(2000, 1, 1, 12, 0, 0, 0, time.UTC)
var window5Min = `"9.467277e+08", "9.46728e+08"`
func TestCrit(t *testing.T) {
+ defer setup()()
s := testSched(t, &schedTest{
conf: `alert a {
crit = avg(q("avg:m{a=b}", "5m", "")) > 0
@@ -233,6 +176,7 @@ func TestCrit(t *testing.T) {
}
func TestBandDisableUnjoined(t *testing.T) {
+ defer setup()()
testSched(t, &schedTest{
conf: `alert a {
$sum = "sum:m{a=*}"
@@ -259,6 +203,7 @@ func TestBandDisableUnjoined(t *testing.T) {
}
func TestCount(t *testing.T) {
+ defer setup()()
testSched(t, &schedTest{
conf: `alert a {
crit = count("sum:m{a=*}", "5m", "") != 2
@@ -281,13 +226,7 @@ func TestCount(t *testing.T) {
}
func TestUnknown(t *testing.T) {
- state := NewStatus("a{a=b}")
- state.Touched = queryTime.Add(-10 * time.Minute)
- state.Append(&Event{Status: StNormal, Time: state.Touched})
- stillValid := NewStatus("a{a=c}")
- stillValid.Touched = queryTime.Add(-9 * time.Minute)
- stillValid.Append(&Event{Status: StNormal, Time: stillValid.Touched})
-
+ defer setup()()
testSched(t, &schedTest{
conf: `alert a {
crit = avg(q("avg:m{a=*}", "5m", "")) > 0
@@ -298,21 +237,15 @@ func TestUnknown(t *testing.T) {
state: map[schedState]bool{
schedState{"a{a=b}", "unknown"}: true,
},
- previous: map[models.AlertKey]*State{
- "a{a=b}": state,
- "a{a=c}": stillValid,
+ touched: map[models.AlertKey]time.Time{
+ "a{a=b}": queryTime.Add(-10 * time.Minute),
+ "a{a=c}": queryTime.Add(-9 * time.Minute),
},
})
}
func TestUnknown_HalfFreq(t *testing.T) {
- state := NewStatus("a{a=b}")
- state.Touched = queryTime.Add(-20 * time.Minute)
- state.Append(&Event{Status: StNormal, Time: state.Touched})
- stillValid := NewStatus("a{a=c}")
- stillValid.Touched = queryTime.Add(-19 * time.Minute)
- stillValid.Append(&Event{Status: StNormal, Time: stillValid.Touched})
-
+ defer setup()()
testSched(t, &schedTest{
conf: `alert a {
crit = avg(q("avg:m{a=*}", "5m", "")) > 0
@@ -324,17 +257,15 @@ func TestUnknown_HalfFreq(t *testing.T) {
state: map[schedState]bool{
schedState{"a{a=b}", "unknown"}: true,
},
- previous: map[models.AlertKey]*State{
- "a{a=b}": state,
- "a{a=c}": stillValid,
+ touched: map[models.AlertKey]time.Time{
+ "a{a=b}": queryTime.Add(-20 * time.Minute),
+ "a{a=c}": queryTime.Add(-19 * time.Minute),
},
})
}
func TestUnknown_WithError(t *testing.T) {
- state := NewStatus("a{a=b}")
- state.Touched = queryTime.Add(-10 * time.Minute)
- state.Append(&Event{Status: StNormal, Time: state.Touched})
+ defer setup()()
s := testSched(t, &schedTest{
conf: `alert a {
@@ -344,16 +275,18 @@ func TestUnknown_WithError(t *testing.T) {
`q("avg:m{a=*}", ` + window5Min + `)`: nil,
},
state: map[schedState]bool{},
- previous: map[models.AlertKey]*State{
- "a{a=b}": state,
+ touched: map[models.AlertKey]time.Time{
+ "a{a=b}": queryTime.Add(-10 * time.Minute),
},
})
+
if s.AlertSuccessful("a") {
t.Fatal("Expected alert a to be in a failed state")
}
}
func TestRename(t *testing.T) {
+ defer setup()()
testSched(t, &schedTest{
conf: `
alert ping.host {
diff --git a/cmd/bosun/sched/silence.go b/cmd/bosun/sched/silence.go
index 7639bb2edc..f380fb2a4b 100644
--- a/cmd/bosun/sched/silence.go
+++ b/cmd/bosun/sched/silence.go
@@ -9,32 +9,31 @@ import (
"bosun.org/slog"
)
-// Silenced returns all currently silenced AlertKeys and the time they will be
-// unsilenced.
-func (s *Schedule) Silenced() map[models.AlertKey]models.Silence {
- aks := make(map[models.AlertKey]models.Silence)
+type SilenceTester func(models.AlertKey) *models.Silence
+// Silenced returns a function that will determine if the given alert key is silenced at the current time.
+// A function is returned to avoid needing to enumerate all alert keys unneccesarily.
+func (s *Schedule) Silenced() SilenceTester {
now := time.Now()
silences, err := s.DataAccess.Silence().GetActiveSilences()
if err != nil {
slog.Error("Error fetching silences.", err)
return nil
}
- for _, si := range silences {
- if !si.ActiveAt(now) {
- continue
- }
- s.Lock("Silence")
- for ak := range s.status {
+ return func(ak models.AlertKey) *models.Silence {
+ var lastEnding *models.Silence
+ for _, si := range silences {
+ if !si.ActiveAt(now) {
+ continue
+ }
if si.Silenced(now, ak.Name(), ak.Group()) {
- if aks[ak].End.Before(si.End) {
- aks[ak] = *si
+ if lastEnding == nil || lastEnding.End.Before(si.End) {
+ lastEnding = si
}
}
}
- s.Unlock()
+ return lastEnding
}
- return aks
}
func (s *Schedule) AddSilence(start, end time.Time, alert, tagList string, forget, confirm bool, edit, user, message string) (map[models.AlertKey]bool, error) {
@@ -82,9 +81,13 @@ func (s *Schedule) AddSilence(start, end time.Time, alert, tagList string, forge
return nil, nil
}
aks := make(map[models.AlertKey]bool)
- for ak := range s.status {
- if si.Matches(ak.Name(), ak.Group()) {
- aks[ak] = s.status[ak].IsActive()
+ open, err := s.DataAccess.State().GetAllOpenIncidents()
+ if err != nil {
+ return nil, err
+ }
+ for _, inc := range open {
+ if si.Matches(inc.Alert, inc.AlertKey.Group()) {
+ aks[inc.AlertKey] = true
}
}
return aks, nil
diff --git a/cmd/bosun/sched/template.go b/cmd/bosun/sched/template.go
index cbe16e48aa..c59657415c 100644
--- a/cmd/bosun/sched/template.go
+++ b/cmd/bosun/sched/template.go
@@ -17,29 +17,28 @@ import (
"bosun.org/_third_party/github.com/jmoiron/jsonq"
"bosun.org/cmd/bosun/conf"
"bosun.org/cmd/bosun/expr"
- "bosun.org/cmd/bosun/expr/parse"
"bosun.org/models"
"bosun.org/opentsdb"
"bosun.org/slog"
)
type Context struct {
- *State
+ *models.IncidentState
Alert *conf.Alert
IsEmail bool
schedule *Schedule
runHistory *RunHistory
- Attachments []*conf.Attachment
+ Attachments []*models.Attachment
}
-func (s *Schedule) Data(rh *RunHistory, st *State, a *conf.Alert, isEmail bool) *Context {
+func (s *Schedule) Data(rh *RunHistory, st *models.IncidentState, a *conf.Alert, isEmail bool) *Context {
c := Context{
- State: st,
- Alert: a,
- IsEmail: isEmail,
- schedule: s,
- runHistory: rh,
+ IncidentState: st,
+ Alert: a,
+ IsEmail: isEmail,
+ schedule: s,
+ runHistory: rh,
}
return &c
}
@@ -65,7 +64,7 @@ func (s *Schedule) unknownData(t time.Time, name string, group models.AlertKeys)
func (c *Context) Ack() string {
return c.schedule.Conf.MakeLink("/action", &url.Values{
"type": []string{"ack"},
- "key": []string{c.Alert.Name + c.State.Group.String()},
+ "key": []string{c.Alert.Name + c.AlertKey.Group().String()},
})
}
@@ -77,13 +76,21 @@ func (c *Context) HostView(host string) string {
})
}
+// Hack so template can read IncidentId off of event.
+func (c *Context) Last() interface{} {
+ return struct {
+ models.Event
+ IncidentId int64
+ }{c.IncidentState.Last(), c.Id}
+}
+
// Expr takes an expression in the form of a string, changes the tags to
// match the context of the alert, and returns a link to the expression page.
func (c *Context) Expr(v string) string {
p := url.Values{}
p.Add("date", c.runHistory.Start.Format(`2006-01-02`))
p.Add("time", c.runHistory.Start.Format(`15:04:05`))
- p.Add("expr", base64.StdEncoding.EncodeToString([]byte(opentsdb.ReplaceTags(v, c.Group))))
+ p.Add("expr", base64.StdEncoding.EncodeToString([]byte(opentsdb.ReplaceTags(v, c.AlertKey.Group()))))
return c.schedule.Conf.MakeLink("/expr", &p)
}
@@ -104,17 +111,17 @@ func (c *Context) Rule() (string, error) {
p.Add("alert", c.Alert.Name)
p.Add("fromDate", time.Format("2006-01-02"))
p.Add("fromTime", time.Format("15:04"))
- p.Add("template_group", c.Group.Tags())
+ p.Add("template_group", c.Tags)
return c.schedule.Conf.MakeLink("/config", &p), nil
}
func (c *Context) Incident() string {
return c.schedule.Conf.MakeLink("/incident", &url.Values{
- "id": []string{fmt.Sprint(c.State.Last().IncidentId)},
+ "id": []string{fmt.Sprint(c.Id)},
})
}
-func (s *Schedule) ExecuteBody(rh *RunHistory, a *conf.Alert, st *State, isEmail bool) ([]byte, []*conf.Attachment, error) {
+func (s *Schedule) ExecuteBody(rh *RunHistory, a *conf.Alert, st *models.IncidentState, isEmail bool) ([]byte, []*models.Attachment, error) {
t := a.Template
if t == nil || t.Body == nil {
return nil, nil, nil
@@ -132,7 +139,7 @@ func (s *Schedule) ExecuteBody(rh *RunHistory, a *conf.Alert, st *State, isEmail
return buf.Bytes(), c.Attachments, nil
}
-func (s *Schedule) ExecuteSubject(rh *RunHistory, a *conf.Alert, st *State, isEmail bool) ([]byte, error) {
+func (s *Schedule) ExecuteSubject(rh *RunHistory, a *conf.Alert, st *models.IncidentState, isEmail bool) ([]byte, error) {
t := a.Template
if t == nil || t.Subject == nil {
return nil, nil
@@ -166,8 +173,8 @@ var error_body = template.Must(template.New("body_error_template").Parse(`
{{end}}`))
-func (s *Schedule) ExecuteBadTemplate(errs []error, rh *RunHistory, a *conf.Alert, st *State) (subject, body []byte, err error) {
- sub := fmt.Sprintf("error: template rendering error for alert %v", st.AlertKey())
+func (s *Schedule) ExecuteBadTemplate(errs []error, rh *RunHistory, a *conf.Alert, st *models.IncidentState) (subject, body []byte, err error) {
+ sub := fmt.Sprintf("error: template rendering error for alert %v", st.AlertKey)
c := struct {
Errors []error
*Context
@@ -183,15 +190,15 @@ func (s *Schedule) ExecuteBadTemplate(errs []error, rh *RunHistory, a *conf.Aler
func (c *Context) evalExpr(e *expr.Expr, filter bool, series bool, autods int) (expr.ResultSlice, string, error) {
var err error
if filter {
- e, err = expr.New(opentsdb.ReplaceTags(e.Text, c.State.Group), c.schedule.Conf.Funcs())
+ e, err = expr.New(opentsdb.ReplaceTags(e.Text, c.AlertKey.Group()), c.schedule.Conf.Funcs())
if err != nil {
return nil, "", err
}
}
- if series && e.Root.Return() != parse.TypeSeriesSet {
+ if series && e.Root.Return() != models.TypeSeriesSet {
return nil, "", fmt.Errorf("need a series, got %T (%v)", e, e)
}
- res, _, err := e.Execute(c.runHistory.Context, c.runHistory.GraphiteContext, c.runHistory.Logstash, c.runHistory.Elastic, c.runHistory.InfluxConfig, c.runHistory.Cache, nil, c.runHistory.Start, autods, c.Alert.UnjoinedOK, c.schedule.Search, c.schedule.Conf.AlertSquelched(c.Alert), c.runHistory)
+ res, _, err := e.Execute(c.runHistory.Context, c.runHistory.GraphiteContext, c.runHistory.Logstash, c.runHistory.Elastic, c.runHistory.InfluxConfig, c.runHistory.Cache, nil, c.runHistory.Start, autods, c.Alert.UnjoinedOK, c.schedule.Search, c.schedule.Conf.AlertSquelched(c.Alert), c.schedule)
if err != nil {
return nil, "", fmt.Errorf("%s: %v", e, err)
}
@@ -218,11 +225,11 @@ func (c *Context) eval(v interface{}, filter bool, series bool, autods int) (res
return nil, "", fmt.Errorf("expected string, expression or resultslice, got %T (%v)", v, v)
}
if filter {
- res = res.Filter(c.State.Group)
+ res = res.Filter(c.AlertKey.Group())
}
if series {
for _, k := range res {
- if k.Type() != parse.TypeSeriesSet {
+ if k.Type() != models.TypeSeriesSet {
return nil, "", fmt.Errorf("need a series, got %v (%v)", k.Type(), k)
}
}
@@ -232,7 +239,7 @@ func (c *Context) eval(v interface{}, filter bool, series bool, autods int) (res
// Lookup returns the value for a key in the lookup table for the context's tagset.
func (c *Context) Lookup(table, key string) (string, error) {
- return c.LookupAll(table, key, c.Group)
+ return c.LookupAll(table, key, c.AlertKey.Group())
}
func (c *Context) LookupAll(table, key string, group interface{}) (string, error) {
@@ -254,7 +261,7 @@ func (c *Context) LookupAll(table, key string, group interface{}) (string, error
if v, ok := l.ToExpr().Get(key, t); ok {
return v, nil
}
- return "", fmt.Errorf("no entry for key %v in table %v for tagset %v", key, table, c.Group)
+ return "", fmt.Errorf("no entry for key %v in table %v for tagset %v", key, table, c.AlertKey.Group())
}
// Eval takes a result or an expression which it evaluates to a result.
@@ -302,7 +309,7 @@ func (c *Context) graph(v interface{}, unit string, filter bool) (val interface{
return nil, err
}
name := fmt.Sprintf("%d.png", len(c.Attachments)+1)
- c.Attachments = append(c.Attachments, &conf.Attachment{
+ c.Attachments = append(c.Attachments, &models.Attachment{
Data: buf.Bytes(),
Filename: name,
ContentType: "image/png",
@@ -469,7 +476,7 @@ func (c *Context) HTTPPost(url, bodyType, data string) string {
func (c *Context) LSQuery(index_root, filter, sduration, eduration string, size int) (interface{}, error) {
var ks []string
- for k, v := range c.Group {
+ for k, v := range c.AlertKey.Group() {
ks = append(ks, k+":"+v)
}
return c.LSQueryAll(index_root, strings.Join(ks, ","), filter, sduration, eduration, size)
@@ -500,7 +507,8 @@ func (c *Context) ESQuery(indexRoot expr.ESIndexer, filter expr.ESQuery, sdurati
if err != nil {
return nil, err
}
- req.Scope(&c.Group)
+ tags := c.Group()
+ req.Scope(&tags)
results, err := c.runHistory.Elastic.Query(req)
if err != nil {
return nil, err
@@ -537,15 +545,15 @@ func (c *Context) ESQueryAll(indexRoot expr.ESIndexer, filter expr.ESQuery, sdur
}
type actionNotificationContext struct {
- States []*State
+ States []*models.IncidentState
User string
Message string
- ActionType ActionType
+ ActionType models.ActionType
schedule *Schedule
}
-func (a actionNotificationContext) IncidentLink(i uint64) string {
+func (a actionNotificationContext) IncidentLink(i int64) string {
return a.schedule.Conf.MakeLink("/incident", &url.Values{
"id": []string{fmt.Sprint(i)},
})
diff --git a/cmd/bosun/search/search_test.go b/cmd/bosun/search/search_test.go
index 7b62736f18..36f46947db 100644
--- a/cmd/bosun/search/search_test.go
+++ b/cmd/bosun/search/search_test.go
@@ -13,7 +13,7 @@ import (
var testSearch *Search
func TestMain(m *testing.M) {
- testData, closeF := dbtest.StartTestRedis()
+ testData, closeF := dbtest.StartTestRedis(9990)
testSearch = NewSearch(testData)
status := m.Run()
closeF()
diff --git a/cmd/bosun/web/chart.go b/cmd/bosun/web/chart.go
index d3cca0a0a1..8060d64eb1 100644
--- a/cmd/bosun/web/chart.go
+++ b/cmd/bosun/web/chart.go
@@ -17,9 +17,9 @@ import (
"bosun.org/_third_party/github.com/vdobler/chart"
"bosun.org/_third_party/github.com/vdobler/chart/svgg"
"bosun.org/cmd/bosun/expr"
- "bosun.org/cmd/bosun/expr/parse"
"bosun.org/cmd/bosun/sched"
"bosun.org/metadata"
+ "bosun.org/models"
"bosun.org/opentsdb"
)
@@ -204,7 +204,7 @@ func ExprGraph(t miniprofiler.Timer, w http.ResponseWriter, r *http.Request) (in
e, err := expr.New(q, schedule.Conf.Funcs())
if err != nil {
return nil, err
- } else if e.Root.Return() != parse.TypeSeriesSet {
+ } else if e.Root.Return() != models.TypeSeriesSet {
return nil, fmt.Errorf("egraph: requires an expression that returns a series")
}
// it may not strictly be necessary to recreate the contexts each time, but we do to be safe
diff --git a/cmd/bosun/web/expr.go b/cmd/bosun/web/expr.go
index 03dce6959e..0991a6f181 100644
--- a/cmd/bosun/web/expr.go
+++ b/cmd/bosun/web/expr.go
@@ -83,7 +83,7 @@ func Expr(t miniprofiler.Timer, w http.ResponseWriter, r *http.Request) (v inter
}
for _, r := range res.Results {
if r.Computations == nil {
- r.Computations = make(expr.Computations, 0)
+ r.Computations = make(models.Computations, 0)
}
}
ret := struct {
@@ -120,7 +120,7 @@ func getTime(r *http.Request) (now time.Time, err error) {
}
type Res struct {
- *sched.Event
+ *models.Event
Key models.AlertKey
}
@@ -132,10 +132,10 @@ func procRule(t miniprofiler.Timer, c *conf.Conf, a *conf.Alert, now time.Time,
return nil, err
}
rh := s.NewRunHistory(now, cacheObj)
- if _, err := s.CheckExpr(t, rh, a, a.Warn, sched.StWarning, nil); err != nil {
+ if _, err := s.CheckExpr(t, rh, a, a.Warn, models.StWarning, nil); err != nil {
return nil, err
}
- if _, err := s.CheckExpr(t, rh, a, a.Crit, sched.StCritical, nil); err != nil {
+ if _, err := s.CheckExpr(t, rh, a, a.Crit, models.StCritical, nil); err != nil {
return nil, err
}
keys := make(models.AlertKeys, len(rh.Events))
@@ -146,11 +146,11 @@ func procRule(t miniprofiler.Timer, c *conf.Conf, a *conf.Alert, now time.Time,
keys[i] = k
i++
switch v.Status {
- case sched.StNormal:
+ case models.StNormal:
normals = append(normals, k)
- case sched.StWarning:
+ case models.StWarning:
warnings = append(warnings, k)
- case sched.StCritical:
+ case models.StCritical:
criticals = append(criticals, k)
default:
return nil, fmt.Errorf("unknown state type %v", v.Status)
@@ -160,8 +160,9 @@ func procRule(t miniprofiler.Timer, c *conf.Conf, a *conf.Alert, now time.Time,
var subject, body []byte
var data interface{}
warning := make([]string, 0)
+
if !summary && len(keys) > 0 {
- var instance *sched.State
+ var primaryIncident *models.IncidentState
if template_group != "" {
ts, err := opentsdb.ParseTags(template_group)
if err != nil {
@@ -169,23 +170,23 @@ func procRule(t miniprofiler.Timer, c *conf.Conf, a *conf.Alert, now time.Time,
}
for _, ak := range keys {
if ak.Group().Subset(ts) {
- instance = s.GetOrCreateStatus(ak)
- instance.History = []sched.Event{*rh.Events[ak]}
+ primaryIncident = sched.NewIncident(ak)
+ primaryIncident.Events = []models.Event{*rh.Events[ak]}
break
}
}
}
- if instance == nil {
- instance = s.GetOrCreateStatus(keys[0])
- instance.History = []sched.Event{*rh.Events[keys[0]]}
+ if primaryIncident == nil {
+ primaryIncident = sched.NewIncident(keys[0])
+ primaryIncident.Events = []models.Event{*rh.Events[keys[0]]}
if template_group != "" {
warning = append(warning, fmt.Sprintf("template group %s was not a subset of any result", template_group))
}
}
- if e := instance.History[0]; e.Crit != nil {
- instance.Result = e.Crit
+ if e := primaryIncident.Events[0]; e.Crit != nil {
+ primaryIncident.Result = e.Crit
} else if e.Warn != nil {
- instance.Result = e.Warn
+ primaryIncident.Result = e.Warn
}
var b_err, s_err error
func() {
@@ -196,7 +197,7 @@ func procRule(t miniprofiler.Timer, c *conf.Conf, a *conf.Alert, now time.Time,
b_err = fmt.Errorf(s)
}
}()
- if body, _, b_err = s.ExecuteBody(rh, a, instance, false); b_err != nil {
+ if body, _, b_err = s.ExecuteBody(rh, a, primaryIncident, false); b_err != nil {
warning = append(warning, b_err.Error())
}
}()
@@ -208,14 +209,14 @@ func procRule(t miniprofiler.Timer, c *conf.Conf, a *conf.Alert, now time.Time,
s_err = fmt.Errorf(s)
}
}()
- subject, s_err = s.ExecuteSubject(rh, a, instance, false)
+ subject, s_err = s.ExecuteSubject(rh, a, primaryIncident, false)
if s_err != nil {
warning = append(warning, s_err.Error())
}
}()
if s_err != nil || b_err != nil {
var err error
- subject, body, err = s.ExecuteBadTemplate([]error{s_err, b_err}, rh, a, instance)
+ subject, body, err = s.ExecuteBadTemplate([]error{s_err, b_err}, rh, a, primaryIncident)
if err != nil {
subject = []byte(fmt.Sprintf("unable to create tempalate error notification: %v", err))
}
@@ -227,17 +228,17 @@ func procRule(t miniprofiler.Timer, c *conf.Conf, a *conf.Alert, now time.Time,
n := conf.Notification{
Email: []*mail.Address{m},
}
- email, attachments, b_err := s.ExecuteBody(rh, a, instance, true)
- email_subject, s_err := s.ExecuteSubject(rh, a, instance, true)
+ email, attachments, b_err := s.ExecuteBody(rh, a, primaryIncident, true)
+ email_subject, s_err := s.ExecuteSubject(rh, a, primaryIncident, true)
if b_err != nil {
warning = append(warning, b_err.Error())
} else if s_err != nil {
warning = append(warning, s_err.Error())
} else {
- n.DoEmail(email_subject, email, schedule.Conf, string(instance.AlertKey()), attachments...)
+ n.DoEmail(email_subject, email, schedule.Conf, string(primaryIncident.AlertKey), attachments...)
}
}
- data = s.Data(rh, instance, a, false)
+ data = s.Data(rh, primaryIncident, a, false)
}
return &ruleResult{
criticals,
@@ -261,7 +262,7 @@ type ruleResult struct {
Body string
Subject string
Data interface{}
- Result map[models.AlertKey]*sched.Event
+ Result map[models.AlertKey]*models.Event
Warning []string
}
@@ -335,7 +336,7 @@ func Rule(t miniprofiler.Timer, w http.ResponseWriter, r *http.Request) (interfa
close(resch)
type Result struct {
Group models.AlertKey
- Result *sched.Event
+ Result *models.Event
}
type Set struct {
Critical, Warning, Normal int
diff --git a/cmd/bosun/web/relay_test.go b/cmd/bosun/web/relay_test.go
index 3c8298d268..d5221b081a 100644
--- a/cmd/bosun/web/relay_test.go
+++ b/cmd/bosun/web/relay_test.go
@@ -20,7 +20,7 @@ var testData database.DataAccess
func TestMain(m *testing.M) {
var closeF func()
- testData, closeF = dbtest.StartTestRedis()
+ testData, closeF = dbtest.StartTestRedis(9991)
status := m.Run()
closeF()
os.Exit(status)
diff --git a/cmd/bosun/web/static.go b/cmd/bosun/web/static.go
index bb256814a2..3f399637c5 100644
--- a/cmd/bosun/web/static.go
+++ b/cmd/bosun/web/static.go
@@ -9099,359 +9099,359 @@ xitlUKAxazlo8/8JAAD//4NB/DtLfAAA
"/js/bosun.js": {
local: "web/static/js/bosun.js",
- size: 108561,
+ size: 108586,
modtime: 0,
compressed: `
-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
+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
-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
-AA==
+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
+fTpYxIhJyP8/AAD//2VI8q8qqAEA
`,
},
@@ -10860,21 +10860,21 @@ C1hENQAA
"/js/history.ts": {
local: "web/static/js/history.ts",
- size: 1452,
+ size: 1480,
modtime: 0,
compressed: `
-H4sIAAAJbogA/2RUXWvjOhB9dn7FXChXMjco976mt126pUsD+wHt9qmEojqTWI0tGUkOG0r++44+nLjZ
-F1senTln5ngkpT3atawQFvfKeWP3j5XpEPCXR71ysPhsXK9T7H1SyO0cnLdKby7po0HrX+qUNgep9xRE
-a409gQ6TyWtguOk6sVIWK692yJl3NyE5S7Ip8BKuroOCRd9bHVaFx7ZrpMcn28yBzTppvZKNm0XdLCtq
-3zZsOikOpFVeZrVbo701DeGcqI5rzrLerbeUA8/swoXGaMkuau+7uGhMJb0yOn5Y0/uwv+51FYI8Zcw/
-ujWFmD4HvRGLe1o9ot2pKsQHtrT3NX+d9qNA3IwrsXgIr7xfBht20oJDaasark58IoU4tRwRW9y7+AsI
-9E5eFGoNXOpN30grlLuxVu55yhGELSN1MQDWxt5JYjsBRi3vErYIEs+7JQl42yNJFMHw4gDYuDgcCXHi
-GEEPqUr6hbJ1FP7x+kajEECOh0cpWtlxvostpFGAPAlMbq8Y/AOoK7PCp4fFrWk7o1F7quwSDqV4M0pz
-9jcL1cQfITboOZvJTs2cl753nwJBEi+pTuH6qkLnOF9JL4fJK7LVDVWGq5c4ZR89/dOwkD+2agpym+2K
-f+CvaIncLodgnu9IFlwJqiLPUvKgHntQi5+qRdJvTUsdi95XPMVi64nmvKgj4aiyxDoFRbPWt69ojwWF
-MhXZ8x/8D6dSGtQbXx9BRS3u9CrXckQ9x7xlrCeVMp6G87RRC+WATjbkzzPrg23B+QS9H66ZU40Wd3TA
-kZfTRBNZMlnoajxkZ9xlbhCu4d+hyXS0xYdbjfTPMpPIuMucFy8+wrPvBr5JX9V0/0G85Rx8Mb1esZQa
-z00YwpjA6XmcvzMmeg+n7LCkx+8AAAD//8TExoGsBQAA
+H4sIAAAJbogA/2RUXU/jOhB9Tn+FL0LEEZV7eS23XLGIFZX2Q4LlCVXIJNPG1LEj24m2Qv3vO/5IG7ov
+iT/OnDNzPLZQDsyal0CWD8I6bXZPpW6BwG8HqrJk+UXbTsW1j0nGt3NinRFqc40TCca91jFsTrja4SIY
+o80RtJ9M3jzDbduyShgoneiB5s7e+uAkmU8JLcjixisYcJ1RfpQ5aFrJHTwbOSf5rOXGCS7tLOgmWVa7
+RubTSbZHreI6qd1p5YyWiLOsPIxpnvTunMEY8pKfW18YDvPz2rk2DKQuuRNahYnRnfP7606VfpHGiPln
+t6YkhM+J2rDlA46ewPSi9OsDW9z7lmbH/SAQNsOILR/9L+0X3oaeG2KBm7ImiyMfi0sUSw6ILexsOAIE
+faAXmVgTytWmk9wwYW+N4TsaYxhii0CdDYC1Nvcc2Y6AUcl9xGZe4qVfoYAzHaBE5g3P9gSkDc0REUeO
+EXQfs8Qj5I3F5Z9v79gKHmSp/xSs4S2lfSghtgJJnZDz7SInlwRUqSt4flze6abVCpTDzK7JvmDvWiia
+X+Q+m3AQbAOO5jPeipl13HX2f0+QxC/J2QWXcnF1VmDKzHZlCdZSWnHHhybMMuwaqyUwqTdxx9ebzkJi
+6lC9hjb8bPrfjvrYsZdTwrfJz3BE/wTP+HY1LKYLEMi8bV6V3fdYro0e1WOPavZLNIDyjW4QwjpX0rgW
+rIkspzkNfKO8IumUCGzFrnkDc0jHJynQtCvyHzkkIkFtXH3AZDW7V1XKZAC9hKhVSCbmMW6V06hR/sWA
+jhak6Ynt3jLveoQ+DG/QIUMDPV5+oMU0sgSSxOVLGjfgCXWRyiM35N+hxHjt2acXD+VPIqPIuMgUFx5F
+xOc/NPnOXVnj20jCC2jJV92pKo+h4U75rgwBFL+Hhjxhwv9wA/cr/PwJAAD//9lyYb3IBQAA
`,
},
@@ -10912,16 +10912,16 @@ XbR/WKhgSOzflwTl6s6H9gd5MCA2aP9PAAAA//+qRVFMNBIAAA==
"/js/incident.ts": {
local: "web/static/js/incident.ts",
- size: 684,
+ size: 675,
modtime: 0,
compressed: `
-H4sIAAAJbogA/1xST+vbMAw9J59C+/GDOKy494RsjDJYYKf1OHbwHLU1pHYm22Vl9LvPf+K29NLo6T3p
-SXKVdkgHIRHGUUs1oXZ7aRYE/OtQTxb0kY8586+ukMhQB9aR0se+rtRa0oHQ14DxEoAtSEinjC7wVte/
-jfV6Z7QjM89Ilst7zJpiv3M0Nxv42bzbaBvC5v3k3JKC2UgRmyZAxrvIH7xOTixXdC+rbCDVd2mVbyHa
-I12UjPnSLnPfV/Tgk0MiU8THH/Gz8m28yEUQWBQkTzA8+vGcYm2fFWoKbM5xNcXDHYB9UFNqUeWxeTpu
-0L2dvXVg/bLMVygXji2EhT8e6QqLIHHG8HBvoZrQedJ1daurtCc/omPNVixqW4rtNj/MZzUNzcdgG8q4
-9VKitYxNwokWhk9pljLM3XeAyPNy0f5Jk5sWxdeEnvn1/YvgS4ZRcUsTpIVZ+H01L5cI36wO/55fbf8/
-AAD//+ZmeN+sAgAA
+H4sIAAAJbogA/1xST+vbMAw9J59C+/GDOKy494RsjDJYYKf1OHbwHLU1pHYm22Vl9LvPf+K29BLr6enp
+WXKUdkgHIRHGUUs1oXZ7aRYE/OtQTxb0kY8586+ukMhQB9aR0se+rtQq6UDoa8B4CcAWJKRTRhd4q+vf
+xnq9M9qRmWcky+U9Zk2x3zmamw38bN5ttA1h835ybknBbKSITRMg413kD14nJ5YV3csoG0j6Lo3yLUR7
+pIuSMV/aZe77ih58ckhkivj4Ix4r38aNXASBRUHyBMOjH88p1va5Qk2BzTmupri4A7APakotqnxtnpYb
+6t7O3jqwflnmK5QNxxbCwh+PdIVFkDhjeLi3oCZ0nnRd3eoqzcmP6FizFYvaFrHd5of5rKah+Rhsg4xb
+LyVay9gknGhh+JTuUi5z9x0g8v0TtT7ryvAvGT5XZLdS8DWhyN+ScZqThe+rZ1lAOHN1+Gl+tf3/AAAA
+//81VnrvowIAAA==
`,
},
@@ -12424,7 +12424,7 @@ bNNNMvIm425oAQ2itVF+BNdwA4t/AwAA///HUuwLLBAAAA==
"/js/state.ts": {
local: "web/static/js/state.ts",
- size: 5269,
+ size: 5266,
modtime: 0,
compressed: `
H4sIAAAJbogA/8RYX2/bOBJ/dj8FYwSRhDhKeo/JuYEvzTXFtbdA0z4svMaCkWhZa4k0SMpttvB33xmS
@@ -12449,12 +12449,12 @@ iCz9j0ifO2nX8tAiy4qmoO73uN3+tm73vK4W68lJyyDqlHzrZsd4jCgwQRzHYxfJHcG8SmYiLIZyEYK+
m/zS+umWrqfB+ZBrInemYlUlCVMqhB6B+ofNmOHJ8sG1+RCVhZG0bsaGVMDzEIOcSs/UAwRviCSjpn1r
xDA1NYT/jgxDNrlHJHGWIfWZMaJekzFmBSlb+jfDZfIUzkxgu3//9LJJDgeF1D4zDag5LhFMEvfbi4Ow
5lGvv7Agx9tC//yMByncDgS1ycrbJrPWURv4HZ6b3MaSgbYJCy/D+eTrLowW0WWGaejlb9W/rq6egoEe
-1Rjvo6iwzwUZSlG2p6WzGMWVTpw2w6XFwh9sFYIUvYVi6B/PGC/vLQ/77pPt2MRSg9uqbvJCh8F8YK6+
-8V2QlzZjeRdGg5sZRyrvMuVP19tfwSWvezk1qHcgy6xOQgPb51eW6W5fiQ9wgfjkijQ0oss8u4W8YugH
-JnsfKlcdJgiM6m1nSynK1zB/zFY0ojE2OqykcLP9FX4X799fvH49jro0EfZjNB8erssSyDVZsv7iYmN6
-vmjbyW/Fj/kw048edyMwzdoaxBtP8TBvO4HTdCTf82TtENMu1Dnid7P1m92Kiv8QcAcOJoFTs+fqQ3lt
-HxMd2VPMkjV+JBv40CUZ2jXR0AvcB9/5NtU0BsjtMDNzQflZdgkSOZKhvcL8LMelobLP8q8AAAD//9be
-lySVFAAA
+1Rjvo6iwzwUZSlG2p6WzGMWVTpw2w6XFwu/x2oIFZgu10D+dMd7dWxb23afaMYklBpdV3aQFn/68P1Vf
+9y7IS5uuvNuigc2MF5V3k/Kn6+2v4IbXvZka1DuQZFZnoIHt8yvLdLevwge4PXxyFRq60GWe3UJSMfQD
+k7oP1aoOEwRG9bazpRTla5g/Ziua0JgavVVSuNb+Cr+L9+8vXr8eR12aCPsxmg8P12UJ5JoUWX9usQE9
+X7S95LeCx3yV6ceOuw6YTm0N4o2neJK3nbBp2pHvebJ2iOkV6gTxu9n6zVZFxX8IuAAHk8Cp2XP1oaS2
+j4mObChmyRq/kA185ZIM7ZpoaATug+98mGq6AuR2mJm5nfwsuwSJHMnQ3l9+luPSUNln+VcAAAD//3UY
+gKGSFAAA
`,
},
@@ -13026,27 +13026,27 @@ NeZtQ48ZNx5cB0tkPLvkz/bBIAzIu+Vq0mH5U/wNAAD//wJxdrNjAwAA
"/partials/alertstate.html": {
local: "web/static/partials/alertstate.html",
- size: 3901,
+ size: 3849,
modtime: 0,
compressed: `
-H4sIAAAJbogA/7RWX2/jNgx/zn0KzQc0CTYnaFcUWBG7uA0dVmx72bV7V2zG1iJLmSS39Xr+7qNkubFT
-5/ov99BYFUXyxx8pUouU3ZKEU62jYEMF8DAHmjKRBURkYVfwi11PklIpEOazoabU0+/H4VLej/1Zlqyj
-wMgs4zCZBvGH0SI/7ds2zHCwktGCklzByi5HC41S4mRRcJ0zTeAe1QpqmBSEJfijYKNAo2NNTE4N/gCh
-HJQheJoJgvo0MewWyERIEQqpCsqnRCNMmAUtiIxXm9za6wb3MH7cDjt+Q80yMT73JvComTVBk6Ojgc3v
-IjJuvI7rIF7MbUh7gitKA89FlVNNlgCCaMZBJJC+IohbycsCQrlaIfwkZzydffZWnoP2bwnakV5QtX4x
-86WAW8pLpCT1zJBP9oQmVEFPepdjSJSksAGRIqLKGmkS94oAW5T9FN1s/eyESa0dW21RAEpJ9RvTRqrq
-woURPTwgEJnCpDHjkE/r2lXpEEcrpl6UPkdMCopXeJuIczwjl/ajSSILICslC9RBqSacrVEX/eC+BsVA
-k5QauqQafWldApEK5VheKywyZBI5UNUrOLOoH8vhSjscOzTNaacq0IrO5V0UdEr9SiQMs2au0iD++PAw
-KKnr8ycVhraWTKRR4IuxXP4DiSFfvng4jvLfoerB2aq3HaTkPFQsy02wRecNZDIgRmM9YJF3NzsGm/AW
-8/w0/rCYY9fDz5Pet5Rp5awzrBXrwXWxzjHVbPX2EslDXYRnTcUMnR48/6MXjRabeKGNkiKLHRMEqbAk
-NluL+cabaEDvsfbTo7U+5f/d6YmgBUwDMt+101m9B7TtgHAwwE9aqwVOXGaJv4w6tPekd/QaN4JvFeDf
-DO70OwLs9J957nvPGqpt53EJsi3nV6xx4tuTLdgfnlpo791fJYcbxa2WXZLLlKHWHqU53G/Uhf1Bp0sj
-qW92l7jjHNsFaI1NdZ8F5q/4BUujvXc/iNv/yOTj/sR2Gsk8nraNZyhx28W3uXB+ML4ruV6yNILgX5jC
-ipbcuPW9DjoU+ll+kZbKPTGi4/yonUGd4VPXR4ZmencwXeOey9UxyWWpHll7H4STN0E4cRD0gTCcvQnD
-2UExHL+NiOPDMnFy+rZ0nO6gOHgL/JRYfO9pgti1abJGPb/YbidcamgEzXIrwsdOBqYdyJmS5aZ9cEf4
-4C7FWsg7MQ4a7eb0yxnYfeT8gb2pCfR15Fg90ii+naA24VsQs+tqA3W9MyWHAc9u8NUYxMuKDNixsrru
-vK1GI3ytDg7Trnc7UuOe1nMg/sQRQjPUOh+C4aV9JF/p+Y/fgbS595lz8LN9su2ZEMcnfkTkav7cNMGz
-7awKc1Pwvv0dTP7zfwAAAP//S43Gqj0PAAA=
+H4sIAAAJbogA/7RWX2/jNgx/zn0KzQc0CTYnaFcUWGG7uA0dVmx72bV7V2zG1iJLmSS39Xr+7qNkubFT
+9/r3Hhqrokj++CNFKsrYNUk51ToOtlQADwugGRN5QEQe9gW/2PUsrZQCYT4baio9/34aruTt1J9l6SYO
+jMxzDrN5kHyYRMXx0LZhhoOVTCJKCgVru5xEGqXEyeLgsmCawC2qldQwKQhL8UfBVoFGx5qYghr8AUI5
+KEPwNBME9Wlq2DWQmZAiFFKVlM+JRpiwCDoQOa+3hbXXD+5uer8d9vyGmuVieupN4FGzaIMmBwcjm9/F
+ZNp6nTZBEi1tSI8EV1YGnoqqoJqsAATRjINIIXtBENeSVyWEcr1G+GnBeLb47K08Be3fCrQjvaRq82zm
+KwHXlFdISeaZIZ/sCU2ogoH0psCQKMlgCyJDRLU10ibuBQF2KIcputr52QuTWju22uIAlJLqN6aNVPWZ
+CyO+u0MgMoNZa8YhnzeNq9IxjtZMPSt9jpgMFK/xNhHneEHO7UeTVJZA1kqWqINSTTjboC76wX0NioEm
+GTV0RTX60roCIhXKsbzWWGTIJHKg6hdwZlHfl8OFdjj2aFrSXlWgFV3ImzhoWbnIguTj3V33T9OcPqgj
+1FgxkcWBL7lq9Q+khnz54p06Yn+HeuB0p971iYrzULG8MMEOgzeQy4AYjVnHUu5v9gy2QUTL4jj5EC2x
+t+HnQYdbyax21hlWhPXgelXvmGq3Bnup5KEuw5O2LsZOj57/0Ysm0TaJtFFS5IljgiAVlsR2K1puvYkW
+9CPWfrq3NqT8vxs9E7SEeUCW+3Z6q7eAtn0O3g3wgwZqgROXWeKvnA7tbRgcvcSN4FsF+DeDG/2GAHtd
+Zln4DrOBetdfXIJsY/kVa5z4JmQL9oeHFrqr9lfF4Upxq2WX5DxjqPWI0hJut+rM/qDTlZHUt7Rz3HGO
+7QK0xtb5mAVMAMPGbM5YFveve5BceAmZfRzNJXaIZTLvmshYenaLb3Ot/JB7Uwq9ZGUEwb8wgzWtuHHr
+Wx30iPJz+SyrlHsuxIfFQTdPeoOkaQ4MzfX+kLnEPZeRQ1LISt2z9jYIR6+CcOQg6HfCcPIqDCfviuHw
+dUQcvi8TR8evS8fxHop3b3SfUovvLa0OezNNN6jnF7vtlEsNraBd7kT4cMnBdGM3V7Lado/nGB/PldgI
+eSOmQavdnn4+A/sPlj9wXrSBvowcq0daxdcT1CV8B2JxWW+hafZm4TjgxRW+AINkVZMRO1bWNL0X1GSC
+L8/Rkdn3bgdnMtB6CsSfOChojlqnYzC8dIjkKz3//juSNvcKcw5+tg+zRybE4ZEfEYVaPjVN8Gw3nsLC
+lHxofw+T//wfAAD//0rIvmwJDwAA
`,
},
diff --git a/cmd/bosun/web/static/js/bosun.js b/cmd/bosun/web/static/js/bosun.js
index 3a64c4b3ef..00c2a5d520 100644
--- a/cmd/bosun/web/static/js/bosun.js
+++ b/cmd/bosun/web/static/js/bosun.js
@@ -2246,24 +2246,25 @@ bosunControllers.controller('HistoryCtrl', ['$scope', '$http', '$location', '$ro
keys[search.key] = true;
}
var params = Object.keys(keys).map(function (v) { return 'ak=' + encodeURIComponent(v); }).join('&');
- $http.get('/api/status?' + params)
+ $http.get('/api/status?' + params + "&all=1")
.success(function (data) {
+ console.log(data);
var selected_alerts = {};
angular.forEach(data, function (v, ak) {
if (!keys[ak]) {
return;
}
- v.History.map(function (h) { h.Time = moment.utc(h.Time); });
- angular.forEach(v.History, function (h, i) {
- if (i + 1 < v.History.length) {
- h.EndTime = v.History[i + 1].Time;
+ v.Events.map(function (h) { h.Time = moment.utc(h.Time); });
+ angular.forEach(v.Events, function (h, i) {
+ if (i + 1 < v.Events.length) {
+ h.EndTime = v.Events[i + 1].Time;
}
else {
h.EndTime = moment.utc();
}
});
selected_alerts[ak] = {
- History: v.History.reverse()
+ History: v.Events.reverse()
};
});
if (Object.keys(selected_alerts).length > 0) {
@@ -2432,9 +2433,9 @@ bosunControllers.controller('IncidentCtrl', ['$scope', '$http', '$location', '$r
}
$http.get('/api/incidents/events?id=' + id)
.success(function (data) {
- $scope.incident = data.Incident;
- $scope.events = data.Events;
+ $scope.incident = data;
$scope.actions = data.Actions;
+ $scope.events = data.Events;
})
.error(function (err) {
$scope.error = err;
@@ -2820,10 +2821,10 @@ bosunApp.directive('tsState', ['$sce', '$http', function ($sce, $http) {
return v.replace(/([,{}()])/g, '$1\u200b');
};
scope.state.Touched = moment(scope.state.Touched).utc();
- angular.forEach(scope.state.History, function (v, k) {
+ angular.forEach(scope.state.Events, function (v, k) {
v.Time = moment(v.Time).utc();
});
- scope.state.last = scope.state.History[scope.state.History.length - 1];
+ scope.state.last = scope.state.Events[scope.state.Events.length - 1];
if (scope.state.Actions && scope.state.Actions.length > 0) {
scope.state.LastAction = scope.state.Actions[0];
}
diff --git a/cmd/bosun/web/static/js/history.ts b/cmd/bosun/web/static/js/history.ts
index 1e4536943b..72ee6b8857 100644
--- a/cmd/bosun/web/static/js/history.ts
+++ b/cmd/bosun/web/static/js/history.ts
@@ -21,23 +21,24 @@ bosunControllers.controller('HistoryCtrl', ['$scope', '$http', '$location', '$ro
keys[search.key] = true;
}
var params = Object.keys(keys).map((v: any) => { return 'ak=' + encodeURIComponent(v); }).join('&');
- $http.get('/api/status?' + params)
+ $http.get('/api/status?' + params + "&all=1")
.success((data) => {
+ console.log(data);
var selected_alerts: any = {};
angular.forEach(data, function(v, ak) {
if (!keys[ak]) {
return;
}
- v.History.map((h: any) => { h.Time = moment.utc(h.Time); });
- angular.forEach(v.History, function(h: any, i: number) {
- if (i + 1 < v.History.length) {
- h.EndTime = v.History[i + 1].Time;
+ v.Events.map((h: any) => { h.Time = moment.utc(h.Time); });
+ angular.forEach(v.Events, function(h: any, i: number) {
+ if (i + 1 < v.Events.length) {
+ h.EndTime = v.Events[i + 1].Time;
} else {
h.EndTime = moment.utc();
}
});
selected_alerts[ak] = {
- History: v.History.reverse(),
+ History: v.Events.reverse(),
};
});
if (Object.keys(selected_alerts).length > 0) {
diff --git a/cmd/bosun/web/static/js/incident.ts b/cmd/bosun/web/static/js/incident.ts
index 5f30c2e7ca..6751672004 100644
--- a/cmd/bosun/web/static/js/incident.ts
+++ b/cmd/bosun/web/static/js/incident.ts
@@ -14,9 +14,9 @@ bosunControllers.controller('IncidentCtrl', ['$scope', '$http', '$location', '$r
}
$http.get('/api/incidents/events?id='+id)
.success((data) => {
- $scope.incident = data.Incident;
- $scope.events = data.Events;
+ $scope.incident = data;
$scope.actions = data.Actions;
+ $scope.events = data.Events;
})
.error(err => {
$scope.error = err;
diff --git a/cmd/bosun/web/static/js/state.ts b/cmd/bosun/web/static/js/state.ts
index 631f35fe6c..466e6457a6 100644
--- a/cmd/bosun/web/static/js/state.ts
+++ b/cmd/bosun/web/static/js/state.ts
@@ -140,10 +140,10 @@ bosunApp.directive('tsState', ['$sce', '$http', function($sce: ng.ISCEService, $
return v.replace(/([,{}()])/g, '$1\u200b');
};
scope.state.Touched = moment(scope.state.Touched).utc();
- angular.forEach(scope.state.History, (v, k) => {
+ angular.forEach(scope.state.Events, (v, k) => {
v.Time = moment(v.Time).utc();
});
- scope.state.last = scope.state.History[scope.state.History.length - 1];
+ scope.state.last = scope.state.Events[scope.state.Events.length - 1];
if (scope.state.Actions && scope.state.Actions.length > 0) {
scope.state.LastAction = scope.state.Actions[0];
}
diff --git a/cmd/bosun/web/static/partials/alertstate.html b/cmd/bosun/web/static/partials/alertstate.html
index 2c9ab15c3d..ebf409702c 100644
--- a/cmd/bosun/web/static/partials/alertstate.html
+++ b/cmd/bosun/web/static/partials/alertstate.html
@@ -7,7 +7,7 @@
- #{{state.last.IncidentId}}:
+ #{{state.Id}}:
@@ -41,7 +41,7 @@
Full History,
Rule Editor,
Expression,
- Incident (#)
+ Incident (#)
diff --git a/cmd/bosun/web/web.go b/cmd/bosun/web/web.go
index e5ab477c80..ddddefb829 100644
--- a/cmd/bosun/web/web.go
+++ b/cmd/bosun/web/web.go
@@ -11,6 +11,7 @@ import (
"net/http"
"net/http/httputil"
"net/url"
+ "sort"
"strconv"
"strings"
"time"
@@ -432,56 +433,50 @@ func IncidentEvents(t miniprofiler.Timer, w http.ResponseWriter, r *http.Request
if id == "" {
return nil, fmt.Errorf("id must be specified")
}
- num, err := strconv.ParseUint(id, 10, 64)
+ num, err := strconv.ParseInt(id, 10, 64)
if err != nil {
return nil, err
}
- incident, events, actions, err := schedule.GetIncidentEvents(uint64(num))
- if err != nil {
- return nil, err
- }
- return struct {
- Incident *models.Incident
- Events []sched.Event
- Actions []sched.Action
- }{incident, events, actions}, nil
+ return schedule.DataAccess.State().GetIncidentState(num)
}
func Incidents(t miniprofiler.Timer, w http.ResponseWriter, r *http.Request) (interface{}, error) {
- alert := r.FormValue("alert")
- toTime := time.Now().UTC()
- fromTime := toTime.Add(-14 * 24 * time.Hour) // 2 weeks
-
- if from := r.FormValue("from"); from != "" {
- t, err := time.Parse(tsdbFormatSecs, from)
- if err != nil {
- return nil, err
- }
- fromTime = t
- }
- if to := r.FormValue("to"); to != "" {
- t, err := time.Parse(tsdbFormatSecs, to)
- if err != nil {
- return nil, err
- }
- toTime = t
- }
- incidents, err := schedule.GetIncidents(alert, fromTime, toTime)
- if err != nil {
- return nil, err
- }
- maxIncidents := 200
- if len(incidents) > maxIncidents {
- incidents = incidents[:maxIncidents]
- }
- return incidents, nil
+ // TODO: Incident Search
+ return nil, nil
+ // alert := r.FormValue("alert")
+ // toTime := time.Now().UTC()
+ // fromTime := toTime.Add(-14 * 24 * time.Hour) // 2 weeks
+
+ // if from := r.FormValue("from"); from != "" {
+ // t, err := time.Parse(tsdbFormatSecs, from)
+ // if err != nil {
+ // return nil, err
+ // }
+ // fromTime = t
+ // }
+ // if to := r.FormValue("to"); to != "" {
+ // t, err := time.Parse(tsdbFormatSecs, to)
+ // if err != nil {
+ // return nil, err
+ // }
+ // toTime = t
+ // }
+ // incidents, err := schedule.GetIncidents(alert, fromTime, toTime)
+ // if err != nil {
+ // return nil, err
+ // }
+ // maxIncidents := 200
+ // if len(incidents) > maxIncidents {
+ // incidents = incidents[:maxIncidents]
+ // }
+ // return incidents, nil
}
func Status(t miniprofiler.Timer, w http.ResponseWriter, r *http.Request) (interface{}, error) {
r.ParseForm()
type ExtStatus struct {
AlertName string
- *sched.State
+ *models.IncidentState
}
m := make(map[string]ExtStatus)
for _, k := range r.Form["ak"] {
@@ -489,8 +484,32 @@ func Status(t miniprofiler.Timer, w http.ResponseWriter, r *http.Request) (inter
if err != nil {
return nil, err
}
- st := ExtStatus{State: schedule.GetStatus(ak)}
- if st.State == nil {
+ var state *models.IncidentState
+ if r.FormValue("all") != "" {
+ allInc, err := schedule.DataAccess.State().GetAllIncidents(ak)
+ if err != nil {
+ return nil, err
+ }
+ if len(allInc) == 0 {
+ return nil, fmt.Errorf("No incidents for alert key")
+ }
+ state = allInc[0]
+ allEvents := models.EventsByTime{}
+ for _, inc := range allInc {
+ for _, e := range inc.Events {
+ allEvents = append(allEvents, e)
+ }
+ }
+ sort.Sort(allEvents)
+ state.Events = allEvents
+ } else {
+ state, err = schedule.DataAccess.State().GetLatestIncident(ak)
+ if err != nil {
+ return nil, err
+ }
+ }
+ st := ExtStatus{IncidentState: state}
+ if st.IncidentState == nil {
return nil, fmt.Errorf("unknown alert key: %v", k)
}
st.AlertName = ak.Name()
@@ -511,14 +530,14 @@ func Action(t miniprofiler.Timer, w http.ResponseWriter, r *http.Request) (inter
if err := j.Decode(&data); err != nil {
return nil, err
}
- var at sched.ActionType
+ var at models.ActionType
switch data.Type {
case "ack":
- at = sched.ActionAcknowledge
+ at = models.ActionAcknowledge
case "close":
- at = sched.ActionClose
+ at = models.ActionClose
case "forget":
- at = sched.ActionForget
+ at = models.ActionForget
}
errs := make(MultiError)
r.ParseForm()
@@ -539,7 +558,10 @@ func Action(t miniprofiler.Timer, w http.ResponseWriter, r *http.Request) (inter
return nil, errs
}
if data.Notify && len(successful) != 0 {
- schedule.ActionNotify(at, data.User, data.Message, successful)
+ err := schedule.ActionNotify(at, data.User, data.Message, successful)
+ if err != nil {
+ return nil, err
+ }
}
return nil, nil
}
diff --git a/models/incidents.go b/models/incidents.go
index 8d5c5b69f6..24fe096616 100644
--- a/models/incidents.go
+++ b/models/incidents.go
@@ -1,12 +1,236 @@
package models
import (
+ "encoding/json"
+ "math"
"time"
+
+ "bosun.org/opentsdb"
)
-type Incident struct {
- Id uint64
+type IncidentState struct {
+ Id int64
Start time.Time
End *time.Time
AlertKey AlertKey
+ Alert string // helper data since AlertKeys don't serialize to JSON well
+ Tags string // string representation of Group
+
+ *Result
+
+ // Most recent last.
+ Events []Event `json:",omitempty"`
+ Actions []Action `json:",omitempty"`
+
+ Subject string
+ Body string
+ EmailBody []byte `json:"-"`
+ EmailSubject []byte `json:"-"`
+ Attachments []*Attachment `json:"-"`
+
+ NeedAck bool
+ Open bool
+
+ Unevaluated bool
+
+ CurrentStatus Status
+ WorstStatus Status
+
+ LastAbnormalStatus Status
+ LastAbnormalTime int64
+}
+
+func (s *IncidentState) Group() opentsdb.TagSet {
+ return s.AlertKey.Group()
+}
+
+func (s *IncidentState) Last() Event {
+ if len(s.Events) == 0 {
+ return Event{}
+ }
+ return s.Events[len(s.Events)-1]
+}
+
+func (s *IncidentState) IsActive() bool {
+ return s.CurrentStatus > StNormal
+}
+
+type Event struct {
+ Warn, Crit *Result `json:",omitempty"`
+ Status Status
+ Time time.Time
+ Unevaluated bool
+}
+
+type EventsByTime []Event
+
+func (a EventsByTime) Len() int { return len(a) }
+func (a EventsByTime) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
+func (a EventsByTime) Less(i, j int) bool { return a[i].Time.Before(a[j].Time) }
+
+// custom float type to support json marshalling of NaN
+type Float float64
+
+func (m Float) MarshalJSON() ([]byte, error) {
+ if math.IsNaN(float64(m)) {
+ return []byte("null"), nil
+ }
+ return json.Marshal(float64(m))
+}
+
+func (m *Float) UnmarshalJSON(b []byte) error {
+ if string(b) == "null" {
+ *m = Float(math.NaN())
+ return nil
+ }
+ var f float64
+ err := json.Unmarshal(b, &f)
+ *m = Float(f)
+ return err
+}
+
+type Result struct {
+ Computations `json:",omitempty"`
+ Value Float
+ Expr string
+}
+
+type Computations []Computation
+
+type Computation struct {
+ Text string
+ Value interface{}
+}
+
+type FuncType int
+
+func (f FuncType) String() string {
+ switch f {
+ case TypeNumberSet:
+ return "number"
+ case TypeString:
+ return "string"
+ case TypeSeriesSet:
+ return "series"
+ case TypeScalar:
+ return "scalar"
+ case TypeESQuery:
+ return "esquery"
+ case TypeESIndexer:
+ return "esindexer"
+ default:
+ return "unknown"
+ }
+}
+
+const (
+ TypeString FuncType = iota
+ TypeScalar
+ TypeNumberSet
+ TypeSeriesSet
+ TypeESQuery
+ TypeESIndexer
+)
+
+type Status int
+
+const (
+ StNone Status = iota
+ StNormal
+ StWarning
+ StCritical
+ StUnknown
+)
+
+func (s Status) String() string {
+ switch s {
+ case StNormal:
+ return "normal"
+ case StWarning:
+ return "warning"
+ case StCritical:
+ return "critical"
+ case StUnknown:
+ return "unknown"
+ default:
+ return "none"
+ }
+}
+
+func (s Status) MarshalJSON() ([]byte, error) {
+ return json.Marshal(s.String())
+}
+
+func (s *Status) UnmarshalJSON(b []byte) error {
+ switch string(b) {
+ case `"normal"`:
+ *s = StNormal
+ case `"warning"`:
+ *s = StWarning
+ case `"critical"`:
+ *s = StCritical
+ case `"unknown"`:
+ *s = StUnknown
+ default:
+ *s = StNone
+ }
+ return nil
+}
+
+func (s Status) IsNormal() bool { return s == StNormal }
+func (s Status) IsWarning() bool { return s == StWarning }
+func (s Status) IsCritical() bool { return s == StCritical }
+func (s Status) IsUnknown() bool { return s == StUnknown }
+
+type Action struct {
+ User string
+ Message string
+ Time time.Time
+ Type ActionType
+}
+
+type ActionType int
+
+const (
+ ActionNone ActionType = iota
+ ActionAcknowledge
+ ActionClose
+ ActionForget
+)
+
+func (a ActionType) String() string {
+ switch a {
+ case ActionAcknowledge:
+ return "Acknowledged"
+ case ActionClose:
+ return "Closed"
+ case ActionForget:
+ return "Forgotten"
+ default:
+ return "none"
+ }
+}
+
+func (a ActionType) MarshalJSON() ([]byte, error) {
+ return json.Marshal(a.String())
+}
+
+func (a *ActionType) UnmarshalJSON(b []byte) error {
+ switch string(b) {
+ case `"Acknowledged"`:
+ *a = ActionAcknowledge
+ case `"Closed"`:
+ *a = ActionClose
+ case `"Forgotten"`:
+ *a = ActionForget
+ default:
+ *a = ActionNone
+ }
+ return nil
+}
+
+type Attachment struct {
+ Data []byte
+ Filename string
+ ContentType string
}
diff --git a/slog/slog.go b/slog/slog.go
index 4880beddfe..1d82b15877 100644
--- a/slog/slog.go
+++ b/slog/slog.go
@@ -150,3 +150,30 @@ func outputf(f func(string), format string, v ...interface{}) {
func outputln(f func(string), v ...interface{}) {
out(f, fmt.Sprintln(v...))
}
+
+type wrappedError struct {
+ error
+ line string
+}
+
+func (w wrappedError) Error() string {
+ return fmt.Sprintf("%s: %s", w.line, w.error.Error())
+}
+
+//Helper to wrap an error with relevant line information. Wrap an error when it enters "our" code before passing it up the stack.
+//This will help narrow down the source of the error.
+func Wrap(err error) error {
+ if err == nil {
+ return nil
+ }
+ if _, ok := err.(wrappedError); ok {
+ return err
+ }
+ line := ""
+ if _, filename, l, ok := runtime.Caller(1); ok {
+ line = fmt.Sprintf("%s:%d", filepath.Base(filename), l)
+ } else {
+ return err
+ }
+ return wrappedError{err, line}
+}