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:
parent
1baf98fb1a
commit
ed3fdc747d
|
@ -1 +1,2 @@
|
|||
protoc --go_out=. nflog/nflogpb/nflog.proto
|
||||
protoc --go_out=. silence/silencepb/silence.proto
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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,
|
||||
}
|
|
@ -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;
|
||||
}
|
Loading…
Reference in New Issue