silence: add protobuf-based silence package.

This commit adds an implementation of a silence storage that can
share store and modify silences, share state via a mesh network,
write and load snapshots, and be dynamically queried.
All data formats are based on protocol buffers.
This commit is contained in:
Fabian Reinartz 2016-08-15 16:10:35 +02:00
parent 1baf98fb1a
commit ed3fdc747d
5 changed files with 2022 additions and 0 deletions

View File

@ -1 +1,2 @@
protoc --go_out=. nflog/nflogpb/nflog.proto
protoc --go_out=. silence/silencepb/silence.proto

792
silence/silence.go Normal file
View File

@ -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
}

961
silence/silence_test.go Normal file
View File

@ -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 = &timestamp.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
}

View File

@ -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,
}

View File

@ -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;
}