Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion nflog/nflog.go
Original file line number Diff line number Diff line change
Expand Up @@ -461,7 +461,7 @@ func stateKey(k string, r *pb.Receiver) string {
return fmt.Sprintf("%s:%s", k, receiverKey(r))
}

func (l *Log) Log(r *pb.Receiver, gkey string, firingAlerts, resolvedAlerts []uint64, store *Store, expiry time.Duration) error {
func (l *Log) Log(r *pb.Receiver, gkey string, firingAlerts, resolvedAlerts, mutedAlerts []uint64, store *Store, expiry time.Duration) error {
// Write all st with the same timestamp.
now := l.now()
key := stateKey(gkey, r)
Expand Down Expand Up @@ -494,6 +494,7 @@ func (l *Log) Log(r *pb.Receiver, gkey string, firingAlerts, resolvedAlerts []ui
Timestamp: timestamppb.New(now),
FiringAlerts: firingAlerts,
ResolvedAlerts: resolvedAlerts,
MutedAlerts: mutedAlerts,
ReceiverData: receiverData,
},
ExpiresAt: timestamppb.New(expiresAt),
Expand Down
82 changes: 81 additions & 1 deletion nflog/nflog_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -367,15 +367,95 @@ func TestQuery(t *testing.T) {
// existing entry
firingAlerts := []uint64{1, 2, 3}
resolvedAlerts := []uint64{4, 5}
mutedAlerts := []uint64{6, 7}

err = nl.Log(recv, "key", firingAlerts, resolvedAlerts, nil, 0)
err = nl.Log(recv, "key", firingAlerts, resolvedAlerts, mutedAlerts, nil, 0)
require.NoError(t, err, "logging notification failed")

entries, err := nl.Query(QGroupKey("key"), QReceiver(recv))
require.NoError(t, err, "querying nflog failed")
entry := entries[0]
require.Equal(t, firingAlerts, entry.FiringAlerts)
require.Equal(t, resolvedAlerts, entry.ResolvedAlerts)
require.Equal(t, mutedAlerts, entry.MutedAlerts)
}

// TestLogMutedAlerts checks that muted alerts survive a round trip through the
// wire format, and that an entry logged without them decodes with the field
// unset, as it does for an entry written by a peer that does not know the
// field.
func TestLogMutedAlerts(t *testing.T) {
now := time.Now().UTC()

cases := []struct {
name string
muted []uint64
}{
{name: "with muted alerts", muted: []uint64{6, 7}},
{name: "without muted alerts", muted: nil},
}

for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
in := state{
"key:abc/test/1": &pb.MeshEntry{
Entry: &pb.Entry{
GroupKey: []byte("key"),
Receiver: &pb.Receiver{GroupName: "abc", Integration: "test", Idx: 1},
Timestamp: timestamppb.New(now),
FiringAlerts: []uint64{1, 2, 3},
ResolvedAlerts: []uint64{4, 5},
MutedAlerts: c.muted,
},
ExpiresAt: timestamppb.New(now.Add(time.Minute)),
},
}

msg, err := in.MarshalBinary()
require.NoError(t, err)

out, err := decodeState(bytes.NewReader(msg))
require.NoError(t, err, "decoding message failed")

for id, expected := range in {
actual, ok := out[id]
require.True(t, ok, "entry %s missing from decoded state", id)
require.True(t, proto.Equal(expected, actual), "entry %s mismatch after decoding", id)
require.Equal(t, c.muted, actual.Entry.MutedAlerts)
}
})
}
}

// TestStateMergeMutedAlerts checks that merging stays timestamp-based: the
// newer entry replaces the older one wholesale, muted alerts included, with no
// per-field merging of the alert lists.
func TestStateMergeMutedAlerts(t *testing.T) {
now := time.Now()

newEntry := func(ts time.Time, muted []uint64) *pb.MeshEntry {
return &pb.MeshEntry{
Entry: &pb.Entry{
Timestamp: timestamppb.New(ts),
GroupKey: []byte("key"),
Receiver: &pb.Receiver{GroupName: "a1", Idx: 1, Integration: "integr"},
MutedAlerts: muted,
},
ExpiresAt: timestamppb.New(now.Add(time.Minute)),
}
}

const key = "key:a1/integr/1"

// A newer entry without muted alerts replaces an older one that has them.
res := state{key: newEntry(now, []uint64{1, 2})}
res.merge(newEntry(now.Add(time.Minute), nil), now)
require.Empty(t, res[key].Entry.MutedAlerts)

// An older entry is dropped, so its muted alerts do not resurface.
res = state{key: newEntry(now, nil)}
res.merge(newEntry(now.Add(-time.Minute), []uint64{1, 2}), now)
require.Empty(t, res[key].Entry.MutedAlerts)
}

func TestStateDecodingError(t *testing.T) {
Expand Down
18 changes: 15 additions & 3 deletions nflog/nflogpb/nflog.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions nflog/nflogpb/nflog.proto
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ message Entry {
repeated uint64 resolved_alerts = 7;
// Data specific to the receiver which sent the notification
map<string, ReceiverDataValue> receiver_data = 8;
// MutedAlerts list of hashes of alerts that were muted at the last
// notification time, and therefore excluded from FiringAlerts and
// ResolvedAlerts.
repeated uint64 muted_alerts = 9;
}

// MeshEntry is a wrapper message to communicate a notify log
Expand Down
11 changes: 11 additions & 0 deletions nflog/nflogpb/set.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,17 @@ func (m *Entry) IsResolvedSubset(subset map[uint64]struct{}) bool {
return isSubset(set, subset)
}

// IsMutedSubset returns whether the given subset is a subset of the alerts
// that were muted at the time of the last notification.
func (m *Entry) IsMutedSubset(subset map[uint64]struct{}) bool {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure we'll end up needing this function, I think we should leave it out until it's clear that it'll be used.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed

set := map[uint64]struct{}{}
for i := range m.MutedAlerts {
set[m.MutedAlerts[i]] = struct{}{}
}

return isSubset(set, subset)
}

func isSubset(set, subset map[uint64]struct{}) bool {
for k := range subset {
_, exists := set[k]
Expand Down
44 changes: 44 additions & 0 deletions nflog/nflogpb/set_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,50 @@ func TestIsResolvedSubset(t *testing.T) {
}
}

func TestIsMutedSubset(t *testing.T) {
e := &Entry{
MutedAlerts: []uint64{1, 2, 3},
}

tests := []struct {
subset map[uint64]struct{}
expected bool
}{
{newSubset(), true}, // empty subset
{newSubset(1), true},
{newSubset(2), true},
{newSubset(3), true},
{newSubset(1, 2), true},
{newSubset(1, 2), true},
{newSubset(1, 2, 3), true},
{newSubset(4), false},
{newSubset(1, 5), false},
{newSubset(1, 2, 3, 6), false},
}

for _, test := range tests {
if result := e.IsMutedSubset(test.subset); result != test.expected {
t.Errorf("Expected %t, got %t for subset %v", test.expected, result, elements(test.subset))
}
}
}

// TestIsMutedSubsetWithoutMutedAlerts covers entries written by a peer that
// does not know about the muted_alerts field. Only the empty subset is a
// subset of no muted alerts.
func TestIsMutedSubsetWithoutMutedAlerts(t *testing.T) {
e := &Entry{
FiringAlerts: []uint64{1, 2, 3},
}

if result := e.IsMutedSubset(newSubset()); !result {
t.Errorf("Expected true, got false for the empty subset")
}
if result := e.IsMutedSubset(newSubset(1)); result {
t.Errorf("Expected false, got true for subset [1]")
}
}

func newSubset(elements ...uint64) map[uint64]struct{} {
subset := make(map[uint64]struct{})
for _, el := range elements {
Expand Down
2 changes: 1 addition & 1 deletion notify/notify.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ func (f StageFunc) Exec(ctx context.Context, l *slog.Logger, alerts ...*alert.Al
}

type NotificationLog interface {
Log(r *nflogpb.Receiver, gkey string, firingAlerts, resolvedAlerts []uint64, store *nflog.Store, expiry time.Duration) error
Log(r *nflogpb.Receiver, gkey string, firingAlerts, resolvedAlerts, mutedAlerts []uint64, store *nflog.Store, expiry time.Duration) error
Query(params ...nflog.QueryParam) ([]*nflogpb.Entry, error)
}

Expand Down
16 changes: 9 additions & 7 deletions notify/notify_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,15 +59,15 @@ type testNflog struct {
qres []*nflogpb.Entry
qerr error

logFunc func(r *nflogpb.Receiver, gkey string, firingAlerts, resolvedAlerts []uint64, receiverData *nflog.Store, expiry time.Duration) error
logFunc func(r *nflogpb.Receiver, gkey string, firingAlerts, resolvedAlerts, mutedAlerts []uint64, receiverData *nflog.Store, expiry time.Duration) error
}

func (l *testNflog) Query(p ...nflog.QueryParam) ([]*nflogpb.Entry, error) {
return l.qres, l.qerr
}

func (l *testNflog) Log(r *nflogpb.Receiver, gkey string, firingAlerts, resolvedAlerts []uint64, receiverData *nflog.Store, expiry time.Duration) error {
return l.logFunc(r, gkey, firingAlerts, resolvedAlerts, receiverData, expiry)
func (l *testNflog) Log(r *nflogpb.Receiver, gkey string, firingAlerts, resolvedAlerts, mutedAlerts []uint64, receiverData *nflog.Store, expiry time.Duration) error {
return l.logFunc(r, gkey, firingAlerts, resolvedAlerts, mutedAlerts, receiverData, expiry)
}

func (l *testNflog) GC() (int, error) {
Expand Down Expand Up @@ -667,11 +667,12 @@ func TestSetNotifiesStage(t *testing.T) {
ctx = WithResolvedAlerts(ctx, []uint64{})
ctx = WithRepeatInterval(ctx, time.Hour)

tnflog.logFunc = func(r *nflogpb.Receiver, gkey string, firingAlerts, resolvedAlerts []uint64, receiverData *nflog.Store, expiry time.Duration) error {
tnflog.logFunc = func(r *nflogpb.Receiver, gkey string, firingAlerts, resolvedAlerts, mutedAlerts []uint64, receiverData *nflog.Store, expiry time.Duration) error {
require.Equal(t, s.recv, r)
require.Equal(t, "1", gkey)
require.Equal(t, []uint64{0, 1, 2}, firingAlerts)
require.Equal(t, []uint64{}, resolvedAlerts)
require.Nil(t, mutedAlerts)
require.Equal(t, 2*time.Hour, expiry)
return nil
}
Expand All @@ -683,11 +684,12 @@ func TestSetNotifiesStage(t *testing.T) {
ctx = WithFiringAlerts(ctx, []uint64{})
ctx = WithResolvedAlerts(ctx, []uint64{0, 1, 2})

tnflog.logFunc = func(r *nflogpb.Receiver, gkey string, firingAlerts, resolvedAlerts []uint64, receiverData *nflog.Store, expiry time.Duration) error {
tnflog.logFunc = func(r *nflogpb.Receiver, gkey string, firingAlerts, resolvedAlerts, mutedAlerts []uint64, receiverData *nflog.Store, expiry time.Duration) error {
require.Equal(t, s.recv, r)
require.Equal(t, "1", gkey)
require.Equal(t, []uint64{}, firingAlerts)
require.Equal(t, []uint64{0, 1, 2}, resolvedAlerts)
require.Nil(t, mutedAlerts)
require.Equal(t, 2*time.Hour, expiry)
return nil
}
Expand All @@ -702,7 +704,7 @@ func TestReceiverData_PreservationWhenNotifierDoesNotUpdate(t *testing.T) {
callCount := 0

tnflog := &testNflog{
logFunc: func(r *nflogpb.Receiver, gkey string, firingAlerts, resolvedAlerts []uint64, receiverData *nflog.Store, expiry time.Duration) error {
logFunc: func(r *nflogpb.Receiver, gkey string, firingAlerts, resolvedAlerts, mutedAlerts []uint64, receiverData *nflog.Store, expiry time.Duration) error {
storedData = receiverData
return nil
},
Expand Down Expand Up @@ -919,7 +921,7 @@ func TestNflogStore_NoLeakBetweenNotificationSequences(t *testing.T) {
var capturedStoreValues []map[string]string

tnflog := &testNflog{
logFunc: func(r *nflogpb.Receiver, gkey string, firingAlerts, resolvedAlerts []uint64, receiverData *nflog.Store, expiry time.Duration) error {
logFunc: func(r *nflogpb.Receiver, gkey string, firingAlerts, resolvedAlerts, mutedAlerts []uint64, receiverData *nflog.Store, expiry time.Duration) error {
storedData = receiverData
return nil
},
Expand Down
2 changes: 1 addition & 1 deletion notify/set_notifies_stage.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,5 +76,5 @@ func (n SetNotifiesStage) Exec(ctx context.Context, l *slog.Logger, alerts ...*a

// Extract receiver data from context if present (it's ok for it to be nil).
store, _ := NflogStore(ctx)
return ctx, alerts, n.nflog.Log(n.recv, gkey, firing, resolved, store, expiry)
return ctx, alerts, n.nflog.Log(n.recv, gkey, firing, resolved, nil, store, expiry)
}
Loading