diff --git a/scripts/genproto.sh b/scripts/genproto.sh index d4985258..7c2c90dd 100755 --- a/scripts/genproto.sh +++ b/scripts/genproto.sh @@ -1 +1,2 @@ protoc --go_out=. nflog/nflogpb/nflog.proto +protoc --go_out=. silence/silencepb/silence.proto diff --git a/silence/silence.go b/silence/silence.go new file mode 100644 index 00000000..4816e269 --- /dev/null +++ b/silence/silence.go @@ -0,0 +1,792 @@ +// 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. + +// The silence package provides a storage for silences, which can share its +// state over a mesh network and snapshot it. +package silence + +import ( + "bytes" + "errors" + "fmt" + "io" + "math/rand" + "os" + "regexp" + "sync" + "time" + + "github.com/go-kit/kit/log" + "github.com/golang/protobuf/ptypes" + "github.com/golang/protobuf/ptypes/timestamp" + "github.com/matttproud/golang_protobuf_extensions/pbutil" + pb "github.com/prometheus/alertmanager/silence/silencepb" + "github.com/prometheus/common/model" + "github.com/satori/go.uuid" + "github.com/weaveworks/mesh" +) + +// ErrNotFound is returned if a silence was not found. +var ErrNotFound = fmt.Errorf("not found") + +func utcNow() time.Time { + return time.Now().UTC() +} + +// Silences holds a silence state that can be modified, queried, and snapshottet. +type Silences struct { + logger log.Logger + now func() time.Time + retention time.Duration + + gossip mesh.Gossip // gossip channel for sharing silences + + // We store silences in a map of IDs for now. Currently, the memory + // state is equivalent to the mesh.GossipData representation. + // In the future we'll want support for efficient queries by time + // range and affected labels. + mtx sync.RWMutex + st gossipData +} + +// Options exposes configuration options for creating a new Silences object. +type Options struct { + // A snapshot file or reader from which the initial state is loaded. + // None or only one of them must be set. + SnapshotFile string + SnapshotReader io.Reader + + // Retention time for newly created Silences. Silences may be + // garbage collected after the given duration after they ended. + Retention time.Duration + + // A function creating a mesh.Gossip on being called with a mesh.Gossiper. + Gossip func(g mesh.Gossiper) mesh.Gossip + + // A logger used by background processing. + Logger log.Logger +} + +func (o *Options) validate() error { + if o.SnapshotFile != "" && o.SnapshotReader != nil { + return fmt.Errorf("only one of SnapshotFile and SnapshotReader must be set") + } + return nil +} + +// New returns a new Silences object with the given configuration. +func New(o Options) (*Silences, error) { + if err := o.validate(); err != nil { + return nil, err + } + if o.SnapshotFile != "" { + var err error + if o.SnapshotReader, err = os.Open(o.SnapshotFile); err != nil { + return nil, err + } + } + s := &Silences{ + logger: log.NewNopLogger(), + retention: o.Retention, + now: utcNow, + gossip: nopGossip{}, + st: gossipData{}, + } + if o.Logger != nil { + s.logger = o.Logger + } + if o.Gossip != nil { + s.gossip = o.Gossip(gossiper{s}) + } + if o.SnapshotReader != nil { + if err := s.loadSnapshot(o.SnapshotReader); err != nil { + return s, err + } + } + return s, nil +} + +type nopGossip struct{} + +func (nopGossip) GossipBroadcast(d mesh.GossipData) {} +func (nopGossip) GossipUnicast(mesh.PeerName, []byte) error { return nil } + +// Maintenance garbage collects the silence state at the given interval. If the snapshot +// file is set, a snapshot is written to it afterwards. +// Terminates on receiving from stopc. +func (s *Silences) Maintenance(interval time.Duration, snapf string, stopc <-chan struct{}) { + t := time.NewTicker(interval) + defer t.Stop() + + f := func() error { + start := s.now() + s.logger.Log("msg", "running maintenance") + defer s.logger.Log("msg", "maintenance done", "duration", s.now().Sub(start)) + + if _, err := s.GC(); err != nil { + return err + } + if snapf == "" { + return nil + } + f, err := openReplace(snapf) + if err != nil { + return err + } + // TODO(fabxc): potentially expose snapshot size in log message. + if _, err := s.Snapshot(f); err != nil { + return err + } + return f.Close() + } + +Loop: + for { + select { + case <-stopc: + break Loop + case <-t.C: + if err := f(); err != nil { + s.logger.Log("msg", "running maintenance failed", "err", err) + } + } + } + // No need for final maintenance if we don't want to snapshot. + if snapf == "" { + return + } + if err := f(); err != nil { + s.logger.Log("msg", "creating shutdown snapshot failed", "err", err) + } +} + +// GC runs a garbage collection that removes silences that have ended longer +// than the configured retention time ago. +func (s *Silences) GC() (int, error) { + now, err := s.nowProto() + if err != nil { + return 0, err + } + var n int + + s.mtx.Lock() + defer s.mtx.Unlock() + + for id, sil := range s.st { + if !protoBefore(now, sil.ExpiresAt) { + delete(s.st, id) + n++ + } + } + return n, nil +} + +func protoBefore(a, b *timestamp.Timestamp) bool { + if a.Seconds > b.Seconds { + return false + } + if a.Seconds == b.Seconds { + return a.Nanos < b.Nanos + } + return true +} + +func validateMatcher(m *pb.Matcher) error { + if !model.LabelName(m.Name).IsValid() { + return fmt.Errorf("invalid label name %q", m.Name) + } + switch m.Type { + case pb.Matcher_EQUAL: + if !model.LabelValue(m.Pattern).IsValid() { + return fmt.Errorf("invalid label value %q", m.Pattern) + } + case pb.Matcher_REGEXP: + if _, err := regexp.Compile(m.Pattern); err != nil { + return fmt.Errorf("invalid regular expression %q: %s", m.Pattern, err) + } + default: + return fmt.Errorf("unknown matcher type %q", m.Type) + } + return nil +} + +func validateSilence(s *pb.Silence) error { + if s.Id == "" { + return errors.New("ID missing") + } + if len(s.Matchers) == 0 { + return errors.New("at least one matcher required") + } + for i, m := range s.Matchers { + if err := validateMatcher(m); err != nil { + return fmt.Errorf("invalid label matcher %d: %s", i, err) + } + } + startsAt, err := ptypes.Timestamp(s.StartsAt) + if err != nil { + return fmt.Errorf("invalid start time: %s", err) + } + endsAt, err := ptypes.Timestamp(s.EndsAt) + if err != nil { + return fmt.Errorf("invalid end time: %s", err) + } + if endsAt.Before(startsAt) { + return errors.New("end time must not be before start time") + } + if _, err := ptypes.Timestamp(s.UpdatedAt); err != nil { + return fmt.Errorf("invalid update timestamp: %s", err) + } + return nil +} + +// cloneSilence returns a shallow copy of a silence. +func cloneSilence(sil *pb.Silence) *pb.Silence { + s := *sil + return &s +} + +func (s *Silences) getSilence(id string) (*pb.Silence, bool) { + msil, ok := s.st[id] + if !ok { + return nil, false + } + return msil.Silence, true +} + +func (s *Silences) setSilence(sil *pb.Silence) error { + endsAt, err := ptypes.Timestamp(sil.EndsAt) + if err != nil { + return err + } + expiresAt, err := ptypes.TimestampProto(endsAt.Add(s.retention)) + if err != nil { + return err + } + msil := &pb.MeshSilence{ + Silence: sil, + ExpiresAt: expiresAt, + } + st := gossipData{sil.Id: msil} + + s.st.Merge(st) + s.gossip.GossipBroadcast(st) + + return nil +} + +func (s *Silences) nowProto() (*timestamp.Timestamp, error) { + now := s.now() + return ptypes.TimestampProto(now) +} + +// Create adds a new silence and returns its ID. +func (s *Silences) Create(sil *pb.Silence) (id string, err error) { + if sil.Id != "" { + return "", fmt.Errorf("unexpected ID in new silence") + } + sil.Id = uuid.NewV4().String() + + now, err := s.nowProto() + if err != nil { + return "", err + } + if sil.StartsAt == nil { + sil.StartsAt = now + } else if protoBefore(sil.StartsAt, now) { + return "", fmt.Errorf("new silence must not start in the past") + } + sil.UpdatedAt = now + + if err := validateSilence(sil); err != nil { + return "", fmt.Errorf("invalid silence: %s", err) + } + + s.mtx.Lock() + defer s.mtx.Unlock() + + if err := s.setSilence(sil); err != nil { + return "", err + } + return sil.Id, nil +} + +// Expire the silence with the given ID immediately. +func (s *Silences) Expire(id string) error { + s.mtx.Lock() + defer s.mtx.Unlock() + + sil, ok := s.getSilence(id) + if !ok { + return ErrNotFound + } + + now, err := s.nowProto() + if err != nil { + return err + } + if sil, err = silenceSetTimeRange(sil, now, sil.StartsAt, now); err != nil { + return err + } + return s.setSilence(sil) +} + +// SetTimeRange adjust the time range of a silence if allowed. If start or end +// are zero times, the current value remains unmodified. +func (s *Silences) SetTimeRange(id string, start, end time.Time) error { + now, err := s.nowProto() + if err != nil { + return err + } + s.mtx.Lock() + defer s.mtx.Unlock() + + sil, ok := s.getSilence(id) + if !ok { + return ErrNotFound + } + + // Retrieve protobuf start and end time, default to current value + // of the silence. + var startp, endp *timestamp.Timestamp + if start.IsZero() { + startp = sil.StartsAt + } else if startp, err = ptypes.TimestampProto(start); err != nil { + return err + } + if end.IsZero() { + endp = sil.EndsAt + } else if endp, err = ptypes.TimestampProto(end); err != nil { + return err + } + + if sil, err = silenceSetTimeRange(sil, now, startp, endp); err != nil { + return err + } + return s.setSilence(sil) +} + +func silenceSetTimeRange(sil *pb.Silence, now, start, end *timestamp.Timestamp) (*pb.Silence, error) { + if protoBefore(end, start) { + return nil, fmt.Errorf("end time must not be before start time") + } + // Validate modification based on current silence state. + switch getState(sil, now) { + case StateActive: + if *start != *sil.StartsAt { + return nil, fmt.Errorf("start time of active silence cannot be modified") + } + if protoBefore(end, now) { + return nil, fmt.Errorf("end time cannot be set into the past") + } + case StatePending: + if protoBefore(start, now) { + return nil, fmt.Errorf("start time cannot be set into the past") + } + case StateExpired: + return nil, fmt.Errorf("expired silence must not be modified") + default: + panic("unknown silence state") + } + + sil = cloneSilence(sil) + sil.StartsAt = start + sil.EndsAt = end + sil.UpdatedAt = now + + return sil, nil +} + +// AddComment adds a new comment to the silence with the given ID. +func (s *Silences) AddComment(id string, author, comment string) error { + panic("not implemented") +} + +// QueryParam expresses parameters along which silences are queried. +type QueryParam func(*query) error + +type query struct { + ids []string + filters []silenceFilter +} + +// silenceFilter is a function that returns true if a silence +// should be dropped from a result set for a given time. +type silenceFilter func(*pb.Silence, *timestamp.Timestamp) (bool, error) + +var errNotSupported = errors.New("query parameter not supported") + +// QIDs configures a query to select the given silence IDs. +func QIDs(ids ...string) QueryParam { + return func(q *query) error { + q.ids = append(q.ids, ids...) + return nil + } +} + +// QTimeRange configures a query to search for silences that are active +// in the given time range. +// TODO(fabxc): not supported yet. +func QTimeRange(start, end time.Time) QueryParam { + return func(q *query) error { + return errNotSupported + } +} + +// QMatches returns silences that match the given label set. +func QMatches(set map[string]string) QueryParam { + return func(q *query) error { + f := func(s *pb.Silence, _ *timestamp.Timestamp) (bool, error) { + // TODO(fabxc): we compile every regexp matcher of a silence + // each time we check it against a label set. + // This could be notably slower than the old caching behavior. + // With the protobuf type not being extensible, we need a more + // efficient solution without wrapping the silence in another layer. + for _, m := range s.Matchers { + switch m.Type { + case pb.Matcher_EQUAL: + if set[m.Name] != m.Pattern { + return false, nil + } + case pb.Matcher_REGEXP: + re, err := regexp.Compile(m.Pattern) + if err != nil { + return false, err + } + if !re.MatchString(set[m.Name]) { + return false, nil + } + } + } + // All matchers applied to the given set and the silence + // passes as a result. + return true, nil + } + q.filters = append(q.filters, f) + return nil + } +} + +// SilenceState describes the state of a silence based on its time range. +type SilenceState string + +// The only possible states of a silence w.r.t a timestamp. +const ( + StateActive SilenceState = "active" + StatePending = "pending" + StateExpired = "expired" +) + +// getState returns a silence's SilenceState at the given timestamp. +func getState(sil *pb.Silence, ts *timestamp.Timestamp) SilenceState { + if protoBefore(ts, sil.StartsAt) { + return StatePending + } + if protoBefore(sil.EndsAt, ts) { + return StateExpired + } + return StateActive +} + +// QState filters queried silences by the given states. +func QState(states ...SilenceState) QueryParam { + return func(q *query) error { + f := func(sil *pb.Silence, now *timestamp.Timestamp) (bool, error) { + s := getState(sil, now) + for _, ps := range states { + if s == ps { + return false, nil + } + } + return true, nil + } + q.filters = append(q.filters, f) + return nil + } +} + +// Query for silences based on the given query parameters. +func (s *Silences) Query(params ...QueryParam) ([]*pb.Silence, error) { + q := &query{} + for _, p := range params { + if err := p(q); err != nil { + return nil, err + } + } + nowpb, err := s.nowProto() + if err != nil { + return nil, err + } + return s.query(q, nowpb) +} + +func (s *Silences) query(q *query, now *timestamp.Timestamp) ([]*pb.Silence, error) { + // If we have an ID constraint, all silences are our base set. + // This and the use of post-filter functions is the + // the trivial solution for now. + var res []*pb.Silence + + s.mtx.RLock() + if q.ids != nil { + for _, id := range q.ids { + if s, ok := s.st[string(id)]; ok { + res = append(res, s.Silence) + } + } + } else { + for _, sil := range s.st { + res = append(res, sil.Silence) + } + } + s.mtx.RUnlock() + + var resf []*pb.Silence + for _, sil := range res { + remove := false + for _, f := range q.filters { + ok, err := f(sil, now) + if err != nil { + return nil, err + } + if !ok { + remove = true + break + } + } + if !remove { + resf = append(resf, sil) + } + } + + return resf, nil +} + +// loadSnapshot loads a snapshot generated by Snapshot() into the state. +// Any previous state is wiped. +func (s *Silences) loadSnapshot(r io.Reader) error { + st := gossipData{} + + for { + var s pb.MeshSilence + if _, err := pbutil.ReadDelimited(r, &s); err != nil { + if err == io.EOF { + break + } + return err + } + st[s.Silence.Id] = &s + } + s.mtx.Lock() + s.st = st + s.mtx.Unlock() + + return nil +} + +// Snapshot writes the full internal state into the writer and returns the number of bytes +// written. +func (s *Silences) Snapshot(w io.Writer) (int, error) { + s.mtx.RLock() + defer s.mtx.RUnlock() + + var n int + for _, s := range s.st { + m, err := pbutil.WriteDelimited(w, s) + if err != nil { + return n + m, err + } + n += m + } + return n, nil +} + +type gossiper struct { + *Silences +} + +// Gossip implements the mesh.Gossiper interface. +func (g gossiper) Gossip() mesh.GossipData { + g.mtx.RLock() + defer g.mtx.RUnlock() + + return g.st.clone() +} + +// OnGossip implements the mesh.Gossiper interface. +func (g gossiper) OnGossip(msg []byte) (mesh.GossipData, error) { + gd, err := decodeGossipData(msg) + if err != nil { + return nil, err + } + g.mtx.Lock() + defer g.mtx.Unlock() + + if delta := g.st.mergeDelta(gd); len(delta) > 0 { + return delta, nil + } + return nil, nil +} + +// OnGossipBroadcast implements the mesh.Gossiper interface. +func (g gossiper) OnGossipBroadcast(src mesh.PeerName, msg []byte) (mesh.GossipData, error) { + gd, err := decodeGossipData(msg) + if err != nil { + return nil, err + } + g.mtx.Lock() + defer g.mtx.Unlock() + + return g.st.mergeDelta(gd), nil +} + +// OnGossipUnicast implements the mesh.Gossiper interface. +// It always panics. +func (g gossiper) OnGossipUnicast(src mesh.PeerName, msg []byte) error { + panic("not implemented") +} + +type gossipData map[string]*pb.MeshSilence + +func decodeGossipData(msg []byte) (gossipData, error) { + gd := gossipData{} + rd := bytes.NewReader(msg) + + for { + var s pb.MeshSilence + if _, err := pbutil.ReadDelimited(rd, &s); err != nil { + if err == io.EOF { + break + } + return gd, err + } + gd[s.Silence.Id] = &s + } + return gd, nil +} + +// Encode implements the mesh.GossipData interface. +func (gd gossipData) Encode() [][]byte { + // Split into sub-messages of ~1MB. + const maxSize = 1024 * 1024 + + var ( + buf bytes.Buffer + res [][]byte + n int + ) + for _, s := range gd { + m, err := pbutil.WriteDelimited(&buf, s) + n += m + if err != nil { + // TODO(fabxc): log error and skip entry. Or can this really not happen with a bytes.Buffer? + panic(err) + } + if n > maxSize { + res = append(res, buf.Bytes()) + buf = bytes.Buffer{} + } + } + if buf.Len() > 0 { + res = append(res, buf.Bytes()) + } + return res +} + +func (gd gossipData) clone() gossipData { + res := make(gossipData, len(gd)) + for id, s := range gd { + res[id] = s + } + return res +} + +// Merge the silence set with gosip data and reutrn a new silence state. +func (gd gossipData) Merge(other mesh.GossipData) mesh.GossipData { + for id, s := range other.(gossipData) { + prev, ok := gd[id] + if !ok { + gd[id] = s + continue + } + pts, err := ptypes.Timestamp(prev.Silence.UpdatedAt) + if err != nil { + panic(err) + } + sts, err := ptypes.Timestamp(s.Silence.UpdatedAt) + if err != nil { + panic(err) + } + if pts.Before(sts) { + gd[id] = s + } + } + return gd +} + +// mergeDelta behaves like Merge but returns a gossipData only +// containing things that have changed. +func (gd gossipData) mergeDelta(od gossipData) gossipData { + delta := gossipData{} + for id, s := range od { + prev, ok := gd[id] + if !ok { + gd[id] = s + delta[id] = s + continue + } + pts, err := ptypes.Timestamp(prev.Silence.UpdatedAt) + if err != nil { + panic(err) + } + sts, err := ptypes.Timestamp(s.Silence.UpdatedAt) + if err != nil { + panic(err) + } + if pts.Before(sts) { + gd[id] = s + delta[id] = s + } + } + return delta +} + +// replaceFile wraps a file that is moved to another filename on closing. +type replaceFile struct { + *os.File + filename string +} + +func (f *replaceFile) Close() error { + if err := f.File.Sync(); err != nil { + return err + } + if err := f.File.Close(); err != nil { + return err + } + return os.Rename(f.File.Name(), f.filename) +} + +// openReplace opens a new temporary file that is moved to filename on closing. +func openReplace(filename string) (*replaceFile, error) { + tmpFilename := fmt.Sprintf("%s.%x", filename, uint64(rand.Int63())) + + f, err := os.Create(tmpFilename) + if err != nil { + return nil, err + } + + rf := &replaceFile{ + File: f, + filename: filename, + } + return rf, nil +} diff --git a/silence/silence_test.go b/silence/silence_test.go new file mode 100644 index 00000000..cafc300e --- /dev/null +++ b/silence/silence_test.go @@ -0,0 +1,961 @@ +// 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 silence + +import ( + "bytes" + "io/ioutil" + "os" + "sort" + "strings" + "testing" + "time" + + "github.com/golang/protobuf/ptypes" + "github.com/golang/protobuf/ptypes/timestamp" + pb "github.com/prometheus/alertmanager/silence/silencepb" + "github.com/stretchr/testify/require" + "github.com/weaveworks/mesh" +) + +func TestOptionsValidate(t *testing.T) { + cases := []struct { + options *Options + err string + }{ + { + options: &Options{ + SnapshotReader: &bytes.Buffer{}, + }, + }, + { + options: &Options{ + SnapshotFile: "test.bkp", + }, + }, + { + options: &Options{ + SnapshotFile: "test bkp", + SnapshotReader: &bytes.Buffer{}, + }, + err: "only one of SnapshotFile and SnapshotReader must be set", + }, + } + + for _, c := range cases { + err := c.options.validate() + if err == nil { + if c.err != "" { + t.Errorf("expected error containing %q but got none", c.err) + } + continue + } + if err != nil && c.err == "" { + t.Errorf("unexpected error %q", err) + continue + } + if !strings.Contains(err.Error(), c.err) { + t.Errorf("expected error to contain %q but got %q", c.err, err) + } + } +} + +func TestSilencesGC(t *testing.T) { + s, err := New(Options{}) + require.NoError(t, err) + + now := utcNow() + s.now = func() time.Time { return now } + + newSilence := func(exp time.Time) *pb.MeshSilence { + return &pb.MeshSilence{ExpiresAt: mustTimeProto(exp)} + } + s.st = gossipData{ + "1": newSilence(now), + "2": newSilence(now.Add(-time.Second)), + "3": newSilence(now.Add(time.Second)), + } + want := gossipData{ + "3": newSilence(now.Add(time.Second)), + } + + n, err := s.GC() + require.NoError(t, err) + require.Equal(t, 2, n) + require.Equal(t, want, s.st) +} + +func TestSilencesSnapshot(t *testing.T) { + // Check whether storing and loading the snapshot is symmetric. + now := utcNow() + + cases := []struct { + entries []*pb.MeshSilence + }{ + { + entries: []*pb.MeshSilence{ + { + Silence: &pb.Silence{ + Id: "3be80475-e219-4ee7-b6fc-4b65114e362f", + Matchers: []*pb.Matcher{ + {Name: "label1", Pattern: "val1", Type: pb.Matcher_EQUAL}, + {Name: "label2", Pattern: "val.+", Type: pb.Matcher_REGEXP}, + }, + StartsAt: mustTimeProto(now), + EndsAt: mustTimeProto(now), + UpdatedAt: mustTimeProto(now), + }, + ExpiresAt: mustTimeProto(now), + }, + { + Silence: &pb.Silence{ + Id: "4b1e760d-182c-4980-b873-c1a6827c9817", + Matchers: []*pb.Matcher{ + {Name: "label1", Pattern: "val1", Type: pb.Matcher_EQUAL}, + }, + StartsAt: mustTimeProto(now.Add(time.Hour)), + EndsAt: mustTimeProto(now.Add(2 * time.Hour)), + UpdatedAt: mustTimeProto(now), + }, + ExpiresAt: mustTimeProto(now.Add(24 * time.Hour)), + }, + }, + }, + } + + for _, c := range cases { + f, err := ioutil.TempFile("", "snapshot") + require.NoError(t, err, "creating temp file failed") + + s1 := &Silences{st: gossipData{}} + // Setup internal state manually. + for _, e := range c.entries { + s1.st[e.Silence.Id] = e + } + _, err = s1.Snapshot(f) + require.NoError(t, err, "creating snapshot failed") + + require.NoError(t, f.Close(), "closing snapshot file failed") + + f, err = os.Open(f.Name()) + require.NoError(t, err, "opening snapshot file failed") + + // Check again against new nlog instance. + s2 := &Silences{} + err = s2.loadSnapshot(f) + require.NoError(t, err, "error loading snapshot") + require.Equal(t, s1.st, s2.st, "state after loading snapshot did not match snapshotted state") + + require.NoError(t, f.Close(), "closing snapshot file failed") + } +} + +type mockGossip struct { + broadcast func(mesh.GossipData) +} + +func (g *mockGossip) GossipBroadcast(d mesh.GossipData) { g.broadcast(d) } +func (g *mockGossip) GossipUnicast(mesh.PeerName, []byte) error { panic("not implemented") } + +func TestSilencesSetSilence(t *testing.T) { + s, err := New(Options{ + Retention: time.Minute, + }) + require.NoError(t, err) + + now := utcNow() + nowpb := mustTimeProto(now) + + sil := &pb.Silence{ + Id: "some_id", + EndsAt: nowpb, + } + + want := gossipData{ + "some_id": &pb.MeshSilence{ + Silence: sil, + ExpiresAt: mustTimeProto(now.Add(time.Minute)), + }, + } + + var called bool + s.gossip = &mockGossip{ + broadcast: func(d mesh.GossipData) { + data, ok := d.(gossipData) + require.True(t, ok, "gossip data of unknown type") + require.Equal(t, want, data, "unexpected gossip broadcast data") + + called = true + }, + } + require.NoError(t, s.setSilence(sil)) + require.True(t, called, "GossipBroadcast was not called") + require.Equal(t, want, s.st, "Unexpected silence state") +} + +func TestSilenceCreate(t *testing.T) { + s, err := New(Options{ + Retention: time.Hour, + }) + require.NoError(t, err) + + now := utcNow() + s.now = func() time.Time { return now } + + // Insert silence with fixed start time. + sil1 := &pb.Silence{ + Matchers: []*pb.Matcher{{Name: "a", Pattern: "b"}}, + StartsAt: mustTimeProto(now.Add(2 * time.Minute)), + EndsAt: mustTimeProto(now.Add(5 * time.Minute)), + } + id1, err := s.Create(sil1) + require.NoError(t, err) + require.NotEqual(t, id1, "") + + want := gossipData{ + id1: &pb.MeshSilence{ + Silence: &pb.Silence{ + Id: id1, + Matchers: []*pb.Matcher{{Name: "a", Pattern: "b"}}, + StartsAt: mustTimeProto(now.Add(2 * time.Minute)), + EndsAt: mustTimeProto(now.Add(5 * time.Minute)), + UpdatedAt: mustTimeProto(now), + }, + ExpiresAt: mustTimeProto(now.Add(5*time.Minute + s.retention)), + }, + } + require.Equal(t, want, s.st, "unexpected state after silence creation") + + // Insert silence with unset start time. Must be set to now. + sil2 := &pb.Silence{ + Matchers: []*pb.Matcher{{Name: "a", Pattern: "b"}}, + EndsAt: mustTimeProto(now.Add(1 * time.Minute)), + } + id2, err := s.Create(sil2) + require.NoError(t, err) + require.NotEqual(t, id2, "") + + want = gossipData{ + id1: &pb.MeshSilence{ + Silence: &pb.Silence{ + Id: id1, + Matchers: []*pb.Matcher{{Name: "a", Pattern: "b"}}, + StartsAt: mustTimeProto(now.Add(2 * time.Minute)), + EndsAt: mustTimeProto(now.Add(5 * time.Minute)), + UpdatedAt: mustTimeProto(now), + }, + ExpiresAt: mustTimeProto(now.Add(5*time.Minute + s.retention)), + }, + id2: &pb.MeshSilence{ + Silence: &pb.Silence{ + Id: id2, + Matchers: []*pb.Matcher{{Name: "a", Pattern: "b"}}, + StartsAt: mustTimeProto(now), + EndsAt: mustTimeProto(now.Add(1 * time.Minute)), + UpdatedAt: mustTimeProto(now), + }, + ExpiresAt: mustTimeProto(now.Add(1*time.Minute + s.retention)), + }, + } + require.Equal(t, want, s.st, "unexpected state after silence creation") + +} + +func TestSilencesCreateFail(t *testing.T) { + s, err := New(Options{}) + require.NoError(t, err) + + now := utcNow() + s.now = func() time.Time { return now } + + cases := []struct { + s *pb.Silence + err string + }{ + { + s: &pb.Silence{Id: "some_id"}, + err: "unexpected ID in new silence", + }, { + s: &pb.Silence{StartsAt: mustTimeProto(now.Add(-time.Minute))}, + err: "new silence must not start in the past", + }, { + s: &pb.Silence{}, // Silence without matcher. + err: "invalid silence", + }, + } + for _, c := range cases { + _, err := s.Create(c.s) + if err == nil { + if c.err != "" { + t.Errorf("expected error containing %q but got none", c.err) + } + continue + } + if err != nil && c.err == "" { + t.Errorf("unexpected error %q", err) + continue + } + if !strings.Contains(err.Error(), c.err) { + t.Errorf("expected error to contain %q but got %q", c.err, err) + } + } +} + +func TestQState(t *testing.T) { + now := utcNow() + + cases := []struct { + sil *pb.Silence + states []SilenceState + drop bool + }{ + { + sil: &pb.Silence{ + StartsAt: mustTimeProto(now.Add(time.Minute)), + EndsAt: mustTimeProto(now.Add(time.Hour)), + }, + states: []SilenceState{StateActive, StateExpired}, + drop: true, + }, + { + sil: &pb.Silence{ + StartsAt: mustTimeProto(now.Add(time.Minute)), + EndsAt: mustTimeProto(now.Add(time.Hour)), + }, + states: []SilenceState{StatePending}, + drop: false, + }, + { + sil: &pb.Silence{ + StartsAt: mustTimeProto(now.Add(time.Minute)), + EndsAt: mustTimeProto(now.Add(time.Hour)), + }, + states: []SilenceState{StateExpired, StatePending}, + drop: false, + }, + } + for i, c := range cases { + q := &query{} + QState(c.states...)(q) + f := q.filters[0] + + drop, err := f(c.sil, mustTimeProto(now)) + require.NoError(t, err) + require.Equal(t, c.drop, drop, "unexpected filter result for case %d", i) + } +} + +func TestQMatches(t *testing.T) { + qp := QMatches(map[string]string{ + "job": "test", + "instance": "web-1", + "path": "/user/profile", + "method": "GET", + }) + + q := &query{} + qp(q) + f := q.filters[0] + + cases := []struct { + sil *pb.Silence + drop bool + }{ + { + sil: &pb.Silence{ + Matchers: []*pb.Matcher{ + {Name: "job", Pattern: "test", Type: pb.Matcher_EQUAL}, + }, + }, + drop: true, + }, + { + sil: &pb.Silence{ + Matchers: []*pb.Matcher{ + {Name: "job", Pattern: "test", Type: pb.Matcher_EQUAL}, + {Name: "method", Pattern: "POST", Type: pb.Matcher_EQUAL}, + }, + }, + drop: false, + }, + { + sil: &pb.Silence{ + Matchers: []*pb.Matcher{ + {Name: "path", Pattern: "/user/.+", Type: pb.Matcher_REGEXP}, + }, + }, + drop: true, + }, + { + sil: &pb.Silence{ + Matchers: []*pb.Matcher{ + {Name: "path", Pattern: "/user/.+", Type: pb.Matcher_REGEXP}, + {Name: "path", Pattern: "/nothing/.+", Type: pb.Matcher_REGEXP}, + }, + }, + drop: false, + }, + } + for _, c := range cases { + drop, err := f(c.sil, nil) + require.NoError(t, err) + require.Equal(t, c.drop, drop, "unexpected filter result") + } +} + +func TestSilencesQuery(t *testing.T) { + s, err := New(Options{}) + require.NoError(t, err) + + s.st = gossipData{ + "1": &pb.MeshSilence{Silence: &pb.Silence{Id: "1"}}, + "2": &pb.MeshSilence{Silence: &pb.Silence{Id: "2"}}, + "3": &pb.MeshSilence{Silence: &pb.Silence{Id: "3"}}, + "4": &pb.MeshSilence{Silence: &pb.Silence{Id: "4"}}, + "5": &pb.MeshSilence{Silence: &pb.Silence{Id: "5"}}, + } + cases := []struct { + q *query + exp []*pb.Silence + }{ + { + // Default query of retrieving all silences. + q: &query{}, + exp: []*pb.Silence{ + {Id: "1"}, + {Id: "2"}, + {Id: "3"}, + {Id: "4"}, + {Id: "5"}, + }, + }, + { + // Retrieve by IDs. + q: &query{ + ids: []string{"2", "5"}, + }, + exp: []*pb.Silence{ + {Id: "2"}, + {Id: "5"}, + }, + }, + { + // Retrieve all and filter + q: &query{ + filters: []silenceFilter{ + func(sil *pb.Silence, _ *timestamp.Timestamp) (bool, error) { + return sil.Id == "1" || sil.Id == "2", nil + }, + }, + }, + exp: []*pb.Silence{ + {Id: "1"}, + {Id: "2"}, + }, + }, + { + // Retrieve by IDs and filter + q: &query{ + ids: []string{"2", "5"}, + filters: []silenceFilter{ + func(sil *pb.Silence, _ *timestamp.Timestamp) (bool, error) { + return sil.Id == "1" || sil.Id == "2", nil + }, + }, + }, + exp: []*pb.Silence{ + {Id: "2"}, + }, + }, + } + + for _, c := range cases { + // Run default query of retrieving all silences. + res, err := s.query(c.q, nil) + require.NoError(t, err, "unexpected error on querying") + + // Currently there are no sorting guarantees in the querying API. + sort.Sort(silencesByID(c.exp)) + sort.Sort(silencesByID(res)) + require.Equal(t, c.exp, res, "unexpected silences in result") + } +} + +type silencesByID []*pb.Silence + +func (s silencesByID) Len() int { return len(s) } +func (s silencesByID) Swap(i, j int) { s[i], s[j] = s[j], s[i] } +func (s silencesByID) Less(i, j int) bool { return s[i].Id < s[j].Id } + +func TestSilenceSetTimeRange(t *testing.T) { + now := utcNow() + + cases := []struct { + sil *pb.Silence + start, end *timestamp.Timestamp + err string + }{ + // Bad arguments. + { + sil: &pb.Silence{}, + start: mustTimeProto(now), + end: mustTimeProto(now.Add(-time.Minute)), + err: "end time must not be before start time", + }, + // Expired silence. + { + sil: &pb.Silence{ + StartsAt: mustTimeProto(now.Add(-time.Hour)), + EndsAt: mustTimeProto(now.Add(-time.Second)), + }, + start: mustTimeProto(now), + end: mustTimeProto(now), + err: "expired silence must not be modified", + }, + // Pending silences. + { + sil: &pb.Silence{ + StartsAt: mustTimeProto(now.Add(time.Hour)), + EndsAt: mustTimeProto(now.Add(2 * time.Hour)), + UpdatedAt: mustTimeProto(now.Add(-time.Hour)), + }, + start: mustTimeProto(now.Add(-time.Minute)), + end: mustTimeProto(now.Add(time.Hour)), + err: "start time cannot be set into the past", + }, + { + sil: &pb.Silence{ + StartsAt: mustTimeProto(now.Add(time.Hour)), + EndsAt: mustTimeProto(now.Add(2 * time.Hour)), + UpdatedAt: mustTimeProto(now.Add(-time.Hour)), + }, + start: mustTimeProto(now.Add(time.Minute)), + end: mustTimeProto(now.Add(time.Minute)), + }, + { + sil: &pb.Silence{ + StartsAt: mustTimeProto(now.Add(time.Hour)), + EndsAt: mustTimeProto(now.Add(2 * time.Hour)), + UpdatedAt: mustTimeProto(now.Add(-time.Hour)), + }, + start: mustTimeProto(now), // set to exactly start now. + end: mustTimeProto(now.Add(2 * time.Hour)), + }, + // Active silences. + { + sil: &pb.Silence{ + StartsAt: mustTimeProto(now.Add(-time.Hour)), + EndsAt: mustTimeProto(now.Add(2 * time.Hour)), + UpdatedAt: mustTimeProto(now.Add(-time.Hour)), + }, + start: mustTimeProto(now.Add(-time.Minute)), + end: mustTimeProto(now.Add(2 * time.Hour)), + err: "start time of active silence cannot be modified", + }, + { + sil: &pb.Silence{ + StartsAt: mustTimeProto(now.Add(-time.Hour)), + EndsAt: mustTimeProto(now.Add(2 * time.Hour)), + UpdatedAt: mustTimeProto(now.Add(-time.Hour)), + }, + start: mustTimeProto(now.Add(-time.Hour)), + end: mustTimeProto(now.Add(-time.Second)), + err: "end time cannot be set into the past", + }, + { + sil: &pb.Silence{ + StartsAt: mustTimeProto(now.Add(-time.Hour)), + EndsAt: mustTimeProto(now.Add(2 * time.Hour)), + UpdatedAt: mustTimeProto(now.Add(-time.Hour)), + }, + start: mustTimeProto(now.Add(-time.Hour)), + end: mustTimeProto(now), + }, + { + sil: &pb.Silence{ + StartsAt: mustTimeProto(now.Add(-time.Hour)), + EndsAt: mustTimeProto(now.Add(2 * time.Hour)), + UpdatedAt: mustTimeProto(now.Add(-time.Hour)), + }, + start: mustTimeProto(now.Add(-time.Hour)), + end: mustTimeProto(now.Add(3 * time.Hour)), + }, + } + for _, c := range cases { + origSilence := cloneSilence(c.sil) + + sil, err := silenceSetTimeRange(c.sil, mustTimeProto(now), c.start, c.end) + if err == nil { + if c.err != "" { + t.Errorf("expected error containing %q but got none", c.err) + } + // The original silence must not have been modified. + require.Equal(t, origSilence, c.sil, "original silence illegally modified") + + require.Equal(t, sil.StartsAt, c.start) + require.Equal(t, sil.EndsAt, c.end) + require.Equal(t, sil.UpdatedAt, mustTimeProto(now)) + continue + } + if err != nil && c.err == "" { + t.Errorf("unexpected error %q", err) + continue + } + if !strings.Contains(err.Error(), c.err) { + t.Errorf("expected error to contain %q but got %q", c.err, err) + } + + } +} + +func TestValidateMatcher(t *testing.T) { + cases := []struct { + m *pb.Matcher + err string + }{ + { + m: &pb.Matcher{ + Name: "a", + Pattern: "b", + Type: pb.Matcher_EQUAL, + }, + err: "", + }, { + m: &pb.Matcher{ + Name: "00", + Pattern: "a", + Type: pb.Matcher_EQUAL, + }, + err: "invalid label name", + }, { + m: &pb.Matcher{ + Name: "a", + Pattern: "((", + Type: pb.Matcher_REGEXP, + }, + err: "invalid regular expression", + }, { + m: &pb.Matcher{ + Name: "a", + Pattern: "\xff", + Type: pb.Matcher_EQUAL, + }, + err: "invalid label value", + }, { + m: &pb.Matcher{ + Name: "a", + Pattern: "b", + Type: 333, + }, + err: "unknown matcher type", + }, + } + + for _, c := range cases { + err := validateMatcher(c.m) + if err == nil { + if c.err != "" { + t.Errorf("expected error containing %q but got none", c.err) + } + continue + } + if err != nil && c.err == "" { + t.Errorf("unexpected error %q", err) + continue + } + if !strings.Contains(err.Error(), c.err) { + t.Errorf("expected error to contain %q but got %q", c.err, err) + } + } +} + +func TestValidateSilence(t *testing.T) { + var ( + now = utcNow() + invalidTimestamp = ×tamp.Timestamp{Nanos: 1 << 30} + validTimestamp = mustTimeProto(now) + ) + cases := []struct { + s *pb.Silence + err string + }{ + { + s: &pb.Silence{ + Id: "some_id", + Matchers: []*pb.Matcher{ + &pb.Matcher{Name: "a", Pattern: "b"}, + }, + StartsAt: validTimestamp, + EndsAt: validTimestamp, + UpdatedAt: validTimestamp, + }, + err: "", + }, + { + s: &pb.Silence{ + Id: "", + Matchers: []*pb.Matcher{ + &pb.Matcher{Name: "a", Pattern: "b"}, + }, + StartsAt: validTimestamp, + EndsAt: validTimestamp, + UpdatedAt: validTimestamp, + }, + err: "ID missing", + }, + { + s: &pb.Silence{ + Id: "some_id", + Matchers: []*pb.Matcher{}, + StartsAt: validTimestamp, + EndsAt: validTimestamp, + UpdatedAt: validTimestamp, + }, + err: "at least one matcher required", + }, + { + s: &pb.Silence{ + Id: "some_id", + Matchers: []*pb.Matcher{ + &pb.Matcher{Name: "a", Pattern: "b"}, + &pb.Matcher{Name: "00", Pattern: "b"}, + }, + StartsAt: validTimestamp, + EndsAt: validTimestamp, + UpdatedAt: validTimestamp, + }, + err: "invalid label matcher", + }, + { + s: &pb.Silence{ + Id: "some_id", + Matchers: []*pb.Matcher{ + &pb.Matcher{Name: "a", Pattern: "b"}, + }, + StartsAt: mustTimeProto(now), + EndsAt: mustTimeProto(now.Add(-time.Second)), + UpdatedAt: validTimestamp, + }, + err: "end time must not be before start time", + }, + { + s: &pb.Silence{ + Id: "some_id", + Matchers: []*pb.Matcher{ + &pb.Matcher{Name: "a", Pattern: "b"}, + }, + StartsAt: invalidTimestamp, + EndsAt: validTimestamp, + UpdatedAt: validTimestamp, + }, + err: "invalid start time", + }, + { + s: &pb.Silence{ + Id: "some_id", + Matchers: []*pb.Matcher{ + &pb.Matcher{Name: "a", Pattern: "b"}, + }, + StartsAt: validTimestamp, + EndsAt: invalidTimestamp, + UpdatedAt: validTimestamp, + }, + err: "invalid end time", + }, + { + s: &pb.Silence{ + Id: "some_id", + Matchers: []*pb.Matcher{ + &pb.Matcher{Name: "a", Pattern: "b"}, + }, + StartsAt: validTimestamp, + EndsAt: validTimestamp, + UpdatedAt: invalidTimestamp, + }, + err: "invalid update timestamp", + }, + } + for _, c := range cases { + err := validateSilence(c.s) + if err == nil { + if c.err != "" { + t.Errorf("expected error containing %q but got none", c.err) + } + continue + } + if err != nil && c.err == "" { + t.Errorf("unexpected error %q", err) + continue + } + if !strings.Contains(err.Error(), c.err) { + t.Errorf("expected error to contain %q but got %q", c.err, err) + } + } +} + +func TestGossipDataMerge(t *testing.T) { + now := utcNow() + + // We only care about key names and timestamps for the + // merging logic. + newSilence := func(ts time.Time) *pb.MeshSilence { + return &pb.MeshSilence{ + Silence: &pb.Silence{UpdatedAt: mustTimeProto(ts)}, + } + } + cases := []struct { + a, b gossipData + final, delta gossipData + }{ + { + a: gossipData{ + "a1": newSilence(now), + "a2": newSilence(now), + "a3": newSilence(now), + }, + b: gossipData{ + "b1": newSilence(now), // new key, should be added + "a2": newSilence(now.Add(-time.Minute)), // older timestamp, should be dropped + "a3": newSilence(now.Add(time.Minute)), // newer timestamp, should overwrite + }, + final: gossipData{ + "a1": newSilence(now), + "a2": newSilence(now), + "a3": newSilence(now.Add(time.Minute)), + "b1": newSilence(now), + }, + delta: gossipData{ + "b1": newSilence(now), + "a3": newSilence(now.Add(time.Minute)), + }, + }, + } + + for _, c := range cases { + ca, cb := c.a.clone(), c.b.clone() + + res := ca.Merge(cb) + + require.Equal(t, c.final, res, "Merge result should match expectation") + require.Equal(t, c.final, ca, "Merge should apply changes to original state") + require.Equal(t, c.b, cb, "Merged state should remain unmodified") + + ca, cb = c.a.clone(), c.b.clone() + + delta := ca.mergeDelta(cb) + + require.Equal(t, c.delta, delta, "Merge delta should match expectation") + require.Equal(t, c.final, ca, "Merge should apply changes to original state") + require.Equal(t, c.b, cb, "Merged state should remain unmodified") + } +} + +func TestGossipDataCoding(t *testing.T) { + // Check whether encoding and decoding the data is symmetric. + now := utcNow() + + cases := []struct { + entries []*pb.MeshSilence + }{ + { + entries: []*pb.MeshSilence{ + { + Silence: &pb.Silence{ + Id: "3be80475-e219-4ee7-b6fc-4b65114e362f", + Matchers: []*pb.Matcher{ + {Name: "label1", Pattern: "val1", Type: pb.Matcher_EQUAL}, + {Name: "label2", Pattern: "val.+", Type: pb.Matcher_REGEXP}, + }, + StartsAt: mustTimeProto(now), + EndsAt: mustTimeProto(now), + UpdatedAt: mustTimeProto(now), + }, + ExpiresAt: mustTimeProto(now), + }, + { + Silence: &pb.Silence{ + Id: "4b1e760d-182c-4980-b873-c1a6827c9817", + Matchers: []*pb.Matcher{ + {Name: "label1", Pattern: "val1", Type: pb.Matcher_EQUAL}, + }, + StartsAt: mustTimeProto(now.Add(time.Hour)), + EndsAt: mustTimeProto(now.Add(2 * time.Hour)), + UpdatedAt: mustTimeProto(now), + }, + ExpiresAt: mustTimeProto(now.Add(24 * time.Hour)), + }, + }, + }, + } + + for _, c := range cases { + // Create gossip data from input. + in := gossipData{} + for _, e := range c.entries { + in[e.Silence.Id] = e + } + msg := in.Encode() + require.Equal(t, 1, len(msg), "expected single message for input") + + out, err := decodeGossipData(msg[0]) + require.NoError(t, err, "decoding message failed") + + require.Equal(t, in, out, "decoded data doesn't match encoded data") + } + +} + +func TestProtoBefore(t *testing.T) { + now := utcNow() + nowpb, err := ptypes.TimestampProto(now) + require.NoError(t, err) + + cases := []struct { + ts time.Time + before bool + }{ + { + ts: now.Add(time.Second), + before: true, + }, { + ts: now.Add(-time.Second), + before: false, + }, { + ts: now.Add(time.Nanosecond), + before: true, + }, { + ts: now.Add(-time.Nanosecond), + before: false, + }, { + ts: now, + before: false, + }, + } + + for _, c := range cases { + tspb, err := ptypes.TimestampProto(c.ts) + require.NoError(t, err) + + res := protoBefore(nowpb, tspb) + require.Equal(t, c.before, res, "protoBefore returned unexpected result") + } +} + +func mustTimeProto(ts time.Time) *timestamp.Timestamp { + pt, err := ptypes.TimestampProto(ts) + if err != nil { + panic(err) + } + return pt +} diff --git a/silence/silencepb/silence.pb.go b/silence/silencepb/silence.pb.go new file mode 100644 index 00000000..4679ff3f --- /dev/null +++ b/silence/silencepb/silence.pb.go @@ -0,0 +1,211 @@ +// Code generated by protoc-gen-go. +// source: silence/silencepb/silence.proto +// DO NOT EDIT! + +/* +Package silencepb is a generated protocol buffer package. + +It is generated from these files: + silence/silencepb/silence.proto + +It has these top-level messages: + Matcher + Comment + Silence + MeshSilence +*/ +package silencepb + +import proto "github.com/golang/protobuf/proto" +import fmt "fmt" +import math "math" +import google_protobuf "github.com/golang/protobuf/ptypes/timestamp" + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package + +// Type specifies how the given name and pattern are matched +// against a label set. +type Matcher_Type int32 + +const ( + Matcher_EQUAL Matcher_Type = 0 + Matcher_REGEXP Matcher_Type = 1 +) + +var Matcher_Type_name = map[int32]string{ + 0: "EQUAL", + 1: "REGEXP", +} +var Matcher_Type_value = map[string]int32{ + "EQUAL": 0, + "REGEXP": 1, +} + +func (x Matcher_Type) String() string { + return proto.EnumName(Matcher_Type_name, int32(x)) +} +func (Matcher_Type) EnumDescriptor() ([]byte, []int) { return fileDescriptor0, []int{0, 0} } + +// Matcher specifies a rule, which can match or set of labels or not. +type Matcher struct { + Type Matcher_Type `protobuf:"varint,1,opt,name=type,enum=silencepb.Matcher_Type" json:"type,omitempty"` + // The label name in a label set to against which the matcher + // checks the pattern. + Name string `protobuf:"bytes,2,opt,name=name" json:"name,omitempty"` + // The pattern being checked according to the matcher's type. + Pattern string `protobuf:"bytes,3,opt,name=pattern" json:"pattern,omitempty"` +} + +func (m *Matcher) Reset() { *m = Matcher{} } +func (m *Matcher) String() string { return proto.CompactTextString(m) } +func (*Matcher) ProtoMessage() {} +func (*Matcher) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} } + +// A comment can be attached to a silence. +type Comment struct { + Author string `protobuf:"bytes,1,opt,name=author" json:"author,omitempty"` + Comment string `protobuf:"bytes,2,opt,name=comment" json:"comment,omitempty"` + Timestamp *google_protobuf.Timestamp `protobuf:"bytes,3,opt,name=timestamp" json:"timestamp,omitempty"` +} + +func (m *Comment) Reset() { *m = Comment{} } +func (m *Comment) String() string { return proto.CompactTextString(m) } +func (*Comment) ProtoMessage() {} +func (*Comment) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{1} } + +func (m *Comment) GetTimestamp() *google_protobuf.Timestamp { + if m != nil { + return m.Timestamp + } + return nil +} + +// Silence specifies an object that ignores alerts based +// on a set of matchers during a given time frame. +type Silence struct { + // A globally unique identifier. + Id string `protobuf:"bytes,1,opt,name=id" json:"id,omitempty"` + // A set of matchers all of which have to be true for a silence + // to affect a given label set. + Matchers []*Matcher `protobuf:"bytes,2,rep,name=matchers" json:"matchers,omitempty"` + // The time range during which the silence is active. + StartsAt *google_protobuf.Timestamp `protobuf:"bytes,3,opt,name=starts_at,json=startsAt" json:"starts_at,omitempty"` + EndsAt *google_protobuf.Timestamp `protobuf:"bytes,4,opt,name=ends_at,json=endsAt" json:"ends_at,omitempty"` + // The last motification made to the silence. + UpdatedAt *google_protobuf.Timestamp `protobuf:"bytes,5,opt,name=updated_at,json=updatedAt" json:"updated_at,omitempty"` + // A set of comments made on the silence. + Comments []*Comment `protobuf:"bytes,7,rep,name=comments" json:"comments,omitempty"` +} + +func (m *Silence) Reset() { *m = Silence{} } +func (m *Silence) String() string { return proto.CompactTextString(m) } +func (*Silence) ProtoMessage() {} +func (*Silence) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{2} } + +func (m *Silence) GetMatchers() []*Matcher { + if m != nil { + return m.Matchers + } + return nil +} + +func (m *Silence) GetStartsAt() *google_protobuf.Timestamp { + if m != nil { + return m.StartsAt + } + return nil +} + +func (m *Silence) GetEndsAt() *google_protobuf.Timestamp { + if m != nil { + return m.EndsAt + } + return nil +} + +func (m *Silence) GetUpdatedAt() *google_protobuf.Timestamp { + if m != nil { + return m.UpdatedAt + } + return nil +} + +func (m *Silence) GetComments() []*Comment { + if m != nil { + return m.Comments + } + return nil +} + +// MeshSilence wraps a regular silence with an expiration timestamp +// after which the silence may be garbage collected. +type MeshSilence struct { + Silence *Silence `protobuf:"bytes,1,opt,name=silence" json:"silence,omitempty"` + ExpiresAt *google_protobuf.Timestamp `protobuf:"bytes,2,opt,name=expires_at,json=expiresAt" json:"expires_at,omitempty"` +} + +func (m *MeshSilence) Reset() { *m = MeshSilence{} } +func (m *MeshSilence) String() string { return proto.CompactTextString(m) } +func (*MeshSilence) ProtoMessage() {} +func (*MeshSilence) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{3} } + +func (m *MeshSilence) GetSilence() *Silence { + if m != nil { + return m.Silence + } + return nil +} + +func (m *MeshSilence) GetExpiresAt() *google_protobuf.Timestamp { + if m != nil { + return m.ExpiresAt + } + return nil +} + +func init() { + proto.RegisterType((*Matcher)(nil), "silencepb.Matcher") + proto.RegisterType((*Comment)(nil), "silencepb.Comment") + proto.RegisterType((*Silence)(nil), "silencepb.Silence") + proto.RegisterType((*MeshSilence)(nil), "silencepb.MeshSilence") + proto.RegisterEnum("silencepb.Matcher_Type", Matcher_Type_name, Matcher_Type_value) +} + +func init() { proto.RegisterFile("silence/silencepb/silence.proto", fileDescriptor0) } + +var fileDescriptor0 = []byte{ + // 372 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x8c, 0x91, 0xcf, 0x4b, 0xeb, 0x40, + 0x10, 0xc7, 0x5f, 0xd2, 0x34, 0x69, 0xa6, 0x50, 0xca, 0x1e, 0xde, 0x0b, 0x85, 0x47, 0x25, 0x27, + 0x41, 0xd9, 0x42, 0x7b, 0x50, 0x8f, 0x45, 0x8a, 0x17, 0x0b, 0xba, 0x56, 0xf0, 0x26, 0x69, 0x33, + 0xb6, 0x81, 0xe6, 0x07, 0xc9, 0x44, 0xf4, 0xec, 0x7f, 0xe2, 0x5f, 0xea, 0x66, 0xb3, 0x89, 0x48, + 0x0f, 0xf5, 0x94, 0x99, 0xcc, 0xe7, 0x3b, 0x33, 0xdf, 0x59, 0x18, 0x17, 0xd1, 0x1e, 0x93, 0x0d, + 0x4e, 0xf4, 0x37, 0x5b, 0x37, 0x11, 0xcf, 0xf2, 0x94, 0x52, 0xe6, 0xb6, 0x85, 0xd1, 0x78, 0x9b, + 0xa6, 0xdb, 0x3d, 0x4e, 0x54, 0x61, 0x5d, 0xbe, 0x4c, 0x28, 0x8a, 0xb1, 0xa0, 0x20, 0xce, 0x6a, + 0xd6, 0xff, 0x30, 0xc0, 0x59, 0x06, 0xb4, 0xd9, 0x61, 0xce, 0xce, 0xc0, 0xa2, 0xf7, 0x0c, 0x3d, + 0xe3, 0xc4, 0x38, 0x1d, 0x4c, 0xff, 0xf1, 0xb6, 0x0d, 0xd7, 0x04, 0x5f, 0xc9, 0xb2, 0x50, 0x10, + 0x63, 0x60, 0x25, 0x41, 0x8c, 0x9e, 0x29, 0x61, 0x57, 0xa8, 0x98, 0x79, 0xe0, 0x64, 0x01, 0x11, + 0xe6, 0x89, 0xd7, 0x51, 0xbf, 0x9b, 0xd4, 0xff, 0x0f, 0x56, 0xa5, 0x65, 0x2e, 0x74, 0x17, 0xf7, + 0x8f, 0xf3, 0xdb, 0xe1, 0x1f, 0x06, 0x60, 0x8b, 0xc5, 0xcd, 0xe2, 0xe9, 0x6e, 0x68, 0xf8, 0x25, + 0x38, 0xd7, 0x69, 0x1c, 0x63, 0x42, 0xec, 0x2f, 0xd8, 0x41, 0x49, 0xbb, 0x34, 0x57, 0x6b, 0xb8, + 0x42, 0x67, 0x55, 0xef, 0x4d, 0x8d, 0xe8, 0x91, 0x4d, 0xca, 0x2e, 0xc1, 0x6d, 0x5d, 0xa9, 0xb9, + 0xfd, 0xe9, 0x88, 0xd7, 0xbe, 0x79, 0xe3, 0x9b, 0xaf, 0x1a, 0x42, 0x7c, 0xc3, 0xfe, 0xa7, 0x09, + 0xce, 0x43, 0x6d, 0x92, 0x0d, 0xc0, 0x8c, 0x42, 0x3d, 0x53, 0x46, 0x8c, 0x43, 0x2f, 0xae, 0x5d, + 0x17, 0x72, 0x60, 0x47, 0x36, 0x65, 0x87, 0x07, 0x11, 0x2d, 0xc3, 0x2e, 0xc0, 0x95, 0x4d, 0x73, + 0x2a, 0x9e, 0x03, 0xfa, 0xc5, 0x16, 0xbd, 0x1a, 0x9e, 0x13, 0x9b, 0x81, 0x83, 0x49, 0xa8, 0x64, + 0xd6, 0x51, 0x99, 0x5d, 0xa1, 0x52, 0x74, 0x05, 0x50, 0x66, 0x61, 0x40, 0x18, 0x56, 0xba, 0xee, + 0x71, 0xd3, 0x9a, 0x96, 0x52, 0x69, 0x4c, 0x5f, 0xae, 0xf0, 0x9c, 0x03, 0x63, 0xfa, 0x19, 0x44, + 0xcb, 0xf8, 0xaf, 0xd0, 0x5f, 0x62, 0xb1, 0x6b, 0xee, 0x74, 0x0e, 0x8e, 0xa6, 0xd5, 0xb1, 0x7e, + 0xaa, 0x35, 0x24, 0x1a, 0xa4, 0xda, 0x13, 0xdf, 0xb2, 0x28, 0x47, 0xe5, 0xcf, 0x3c, 0xbe, 0xa7, + 0xa6, 0xe7, 0xb4, 0xb6, 0x55, 0x79, 0xf6, 0x15, 0x00, 0x00, 0xff, 0xff, 0x7d, 0xe9, 0xc3, 0x5f, + 0xef, 0x02, 0x00, 0x00, +} diff --git a/silence/silencepb/silence.proto b/silence/silencepb/silence.proto new file mode 100644 index 00000000..77d59c45 --- /dev/null +++ b/silence/silencepb/silence.proto @@ -0,0 +1,57 @@ +syntax = "proto3"; + +package silencepb; + +import "google/protobuf/timestamp.proto"; + +// Matcher specifies a rule, which can match or set of labels or not. +message Matcher { + // Type specifies how the given name and pattern are matched + // against a label set. + enum Type { + EQUAL = 0; + REGEXP = 1; + }; + Type type = 1; + + // The label name in a label set to against which the matcher + // checks the pattern. + string name = 2; + // The pattern being checked according to the matcher's type. + string pattern = 3; +} + +// A comment can be attached to a silence. +message Comment { + string author = 1; + string comment = 2; + google.protobuf.Timestamp timestamp = 3; +} + +// Silence specifies an object that ignores alerts based +// on a set of matchers during a given time frame. +message Silence { + // A globally unique identifier. + string id = 1; + + // A set of matchers all of which have to be true for a silence + // to affect a given label set. + repeated Matcher matchers = 2; + + // The time range during which the silence is active. + google.protobuf.Timestamp starts_at = 3; + google.protobuf.Timestamp ends_at = 4; + + // The last motification made to the silence. + google.protobuf.Timestamp updated_at = 5; + + // A set of comments made on the silence. + repeated Comment comments = 7; +} + +// MeshSilence wraps a regular silence with an expiration timestamp +// after which the silence may be garbage collected. +message MeshSilence { + Silence silence = 1; + google.protobuf.Timestamp expires_at = 2; +}