diff --git a/api.go b/api.go index 6a4a7af6..fde027e4 100644 --- a/api.go +++ b/api.go @@ -283,14 +283,15 @@ func (api *API) insertAlerts(w http.ResponseWriter, r *http.Request, alerts ...* } func (api *API) addSilence(w http.ResponseWriter, r *http.Request) { - var sil types.Silence - if err := receive(r, &sil); err != nil { + var msil model.Silence + if err := receive(r, &msil); err != nil { respondError(w, apiError{ typ: errorBadData, err: err, }, nil) return } + sil := types.NewSilence(&msil) if sil.CreatedAt.IsZero() { sil.CreatedAt = time.Now() @@ -304,7 +305,7 @@ func (api *API) addSilence(w http.ResponseWriter, r *http.Request) { return } - sid, err := api.silences.Set(&sil) + sid, err := api.silences.Set(sil) if err != nil { respondError(w, apiError{ typ: errorInternal, diff --git a/config/config.go b/config/config.go index 06d648d2..7d8e017b 100644 --- a/config/config.go +++ b/config/config.go @@ -225,6 +225,9 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error { if c.Route == nil { return fmt.Errorf("No routes provided") } + if len(c.Route.Receiver) == 0 { + return fmt.Errorf("Root route must specify a default receiver") + } if len(c.Route.Match) > 0 || len(c.Route.MatchRE) > 0 { return fmt.Errorf("Root route must not have any matchers") } @@ -240,6 +243,9 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error { // checkReceiver returns an error if a node in the routing tree // references a receiver not in the given map. func checkReceiver(r *Route, receivers map[string]struct{}) error { + if r.Receiver == "" { + return nil + } if _, ok := receivers[r.Receiver]; !ok { return fmt.Errorf("Undefined receiver %q used in route", r.Receiver) } diff --git a/config/config_test.go b/config/config_test.go new file mode 100644 index 00000000..f48a3b67 --- /dev/null +++ b/config/config_test.go @@ -0,0 +1,39 @@ +// Copyright 2016 Prometheus Team +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package config + +import ( + "testing" + + "gopkg.in/yaml.v2" +) + +func TestDefaultReceiverExists(t *testing.T) { + in := ` +route: + group_wait: 30s +` + + conf := &Config{} + err := yaml.Unmarshal([]byte(in), conf) + + expected := "Root route must specify a default receiver" + + if err == nil { + t.Fatalf("no error returned, expected:\n%v", expected) + } + if err.Error() != expected { + t.Errorf("\nexpected:\n%v\ngot:\n%v", expected, err.Error()) + } +} diff --git a/inhibit_test.go b/inhibit_test.go index f212471d..f2acebfc 100644 --- a/inhibit_test.go +++ b/inhibit_test.go @@ -124,7 +124,7 @@ func TestInhibitRuleHasEqual(t *testing.T) { } if have := r.hasEqual(c.input); have != c.result { - t.Errorf("Unexpected result %q, expected %q", have, c.result) + t.Errorf("Unexpected result %t, expected %t", have, c.result) } if !reflect.DeepEqual(r.scache, c.initial) { t.Errorf("Cache state unexpectedly changed") diff --git a/provider/boltmem/boltmem.go b/provider/boltmem/boltmem.go index 58b0a066..0c3481f6 100644 --- a/provider/boltmem/boltmem.go +++ b/provider/boltmem/boltmem.go @@ -217,6 +217,9 @@ func (a *Alerts) Put(alerts ...*types.Alert) error { type Silences struct { db *bolt.DB mk types.Marker + + mtx sync.RWMutex + cache map[uint64]*types.Silence } // NewSilences creates a new Silences provider. @@ -229,7 +232,15 @@ func NewSilences(path string, mk types.Marker) (*Silences, error) { _, err := tx.CreateBucketIfNotExists(bktSilences) return err }) - return &Silences{db: db, mk: mk}, err + if err != nil { + return nil, err + } + s := &Silences{ + db: db, + mk: mk, + cache: map[uint64]*types.Silence{}, + } + return s, s.initCache() } // Close the silences provider. @@ -261,7 +272,19 @@ func (s *Silences) Mutes(lset model.LabelSet) bool { // All returns all existing silences. func (s *Silences) All() ([]*types.Silence, error) { - var res []*types.Silence + s.mtx.RLock() + defer s.mtx.RUnlock() + + res := make([]*types.Silence, 0, len(s.cache)) + for _, s := range s.cache { + res = append(res, s) + } + return res, nil +} + +func (s *Silences) initCache() error { + s.mtx.Lock() + defer s.mtx.Unlock() err := s.db.View(func(tx *bolt.Tx) error { b := tx.Bucket(bktSilences) @@ -272,23 +295,21 @@ func (s *Silences) All() ([]*types.Silence, error) { if err := json.Unmarshal(v, &ms); err != nil { return err } - ms.ID = binary.BigEndian.Uint64(k) - - if err := json.Unmarshal(v, &ms); err != nil { - return err - } - - res = append(res, types.NewSilence(&ms)) + // The ID is duplicated in the value and always equal + // to the stored key. + s.cache[ms.ID] = types.NewSilence(&ms) } return nil }) - - return res, err + return err } // Set a new silence. func (s *Silences) Set(sil *types.Silence) (uint64, error) { + s.mtx.Lock() + defer s.mtx.Unlock() + var ( uid uint64 err error @@ -312,11 +333,18 @@ func (s *Silences) Set(sil *types.Silence) (uint64, error) { } return b.Put(k, msb) }) - return uid, err + if err != nil { + return 0, err + } + s.cache[uid] = sil + return uid, nil } // Del removes a silence. func (s *Silences) Del(uid uint64) error { + s.mtx.Lock() + defer s.mtx.Unlock() + err := s.db.Update(func(tx *bolt.Tx) error { b := tx.Bucket(bktSilences) @@ -325,33 +353,23 @@ func (s *Silences) Del(uid uint64) error { return b.Delete(k) }) - return err + if err != nil { + return err + } + delete(s.cache, uid) + return nil } // Get a silence associated with a fingerprint. func (s *Silences) Get(uid uint64) (*types.Silence, error) { - var sil *types.Silence + s.mtx.RLock() + defer s.mtx.RUnlock() - err := s.db.View(func(tx *bolt.Tx) error { - b := tx.Bucket(bktSilences) - - k := make([]byte, 8) - binary.BigEndian.PutUint64(k, uid) - - v := b.Get(k) - if v == nil { - return provider.ErrNotFound - } - var ms model.Silence - - if err := json.Unmarshal(v, &ms); err != nil { - return err - } - sil = types.NewSilence(&ms) - - return nil - }) - return sil, err + sil, ok := s.cache[uid] + if !ok { + return nil, provider.ErrNotFound + } + return sil, nil } // NotificationInfo provides information about pending and successful diff --git a/route_test.go b/route_test.go index f405f09f..3578dfc0 100644 --- a/route_test.go +++ b/route_test.go @@ -65,6 +65,19 @@ routes: group_by: ['foo', 'bar'] group_wait: 2m receiver: 'notify-BC' + +- match: + group_by: 'role' + group_by: ['role'] + + routes: + - match: + env: 'testing' + receiver: 'notify-testing' + routes: + - match: + wait: 'long' + group_wait: 2m ` var ctree config.Route @@ -167,6 +180,51 @@ routes: }, }, }, + { + input: model.LabelSet{ + "group_by": "role", + }, + result: []*RouteOpts{ + { + Receiver: "notify-def", + GroupBy: lset("role"), + GroupWait: def.GroupWait, + GroupInterval: def.GroupInterval, + RepeatInterval: def.RepeatInterval, + }, + }, + }, + { + input: model.LabelSet{ + "env": "testing", + "group_by": "role", + }, + result: []*RouteOpts{ + { + Receiver: "notify-testing", + GroupBy: lset("role"), + GroupWait: def.GroupWait, + GroupInterval: def.GroupInterval, + RepeatInterval: def.RepeatInterval, + }, + }, + }, + { + input: model.LabelSet{ + "env": "testing", + "group_by": "role", + "wait": "long", + }, + result: []*RouteOpts{ + { + Receiver: "notify-testing", + GroupBy: lset("role"), + GroupWait: 2 * time.Minute, + GroupInterval: def.GroupInterval, + RepeatInterval: def.RepeatInterval, + }, + }, + }, } for _, test := range tests {