Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
19 changes: 12 additions & 7 deletions types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -295,9 +295,8 @@ func Alerts(alerts ...*Alert) model.Alerts {
res := make(model.Alerts, 0, len(alerts))
for _, a := range alerts {
v := a.Alert
// If the end timestamp was set as the expected value in case
// of a timeout but is not reached yet, do not expose it.
if a.Timeout && !a.Resolved() {
// If the end timestamp is not reached yet, do not expose it.
if !a.Resolved() {
v.EndsAt = time.Time{}
}
res = append(res, &v)
Expand All @@ -321,10 +320,16 @@ func (a *Alert) Merge(o *Alert) *Alert {
res.StartsAt = a.StartsAt
}

// A non-timeout resolved timestamp always rules.
// The latest explicit resolved timestamp wins.
if a.EndsAt.After(o.EndsAt) && !a.Timeout {
res.EndsAt = a.EndsAt
if o.Resolved() {
// The latest explicit resolved timestamp wins if both alerts are effectively resolved.
if a.Resolved() && a.EndsAt.After(o.EndsAt) {
res.EndsAt = a.EndsAt
}
} else {
// A non-timeout timestamp always rules if it is the latest.
if a.EndsAt.After(o.EndsAt) && !a.Timeout {
res.EndsAt = a.EndsAt
}
}

return &res
Expand Down
206 changes: 201 additions & 5 deletions types/types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package types
import (
"reflect"
"sort"
"strconv"
"testing"
"time"

Expand All @@ -26,13 +27,17 @@ import (
func TestAlertMerge(t *testing.T) {
now := time.Now()

// By convention, alert A is always older than alert B.
pairs := []struct {
A, B, Res *Alert
}{
{
// Both alerts have the Timeout flag set.
// StartsAt is defined by Alert A.
// EndsAt is defined by Alert B.
A: &Alert{
Alert: model.Alert{
StartsAt: now.Add(-time.Minute),
StartsAt: now.Add(-2 * time.Minute),
EndsAt: now.Add(2 * time.Minute),
},
UpdatedAt: now,
Expand All @@ -46,6 +51,61 @@ func TestAlertMerge(t *testing.T) {
UpdatedAt: now.Add(time.Minute),
Timeout: true,
},
Res: &Alert{
Alert: model.Alert{
StartsAt: now.Add(-2 * time.Minute),
EndsAt: now.Add(3 * time.Minute),
},
UpdatedAt: now.Add(time.Minute),
Timeout: true,
},
},
{
// Alert A has the Timeout flag set while Alert B has it unset.
// StartsAt is defined by Alert A.
// EndsAt is defined by Alert B.
A: &Alert{
Alert: model.Alert{
StartsAt: now.Add(-time.Minute),
EndsAt: now.Add(3 * time.Minute),
},
UpdatedAt: now,
Timeout: true,
},
B: &Alert{
Alert: model.Alert{
StartsAt: now,
EndsAt: now.Add(2 * time.Minute),
},
UpdatedAt: now.Add(time.Minute),
},
Res: &Alert{
Alert: model.Alert{
StartsAt: now.Add(-time.Minute),
EndsAt: now.Add(2 * time.Minute),
},
UpdatedAt: now.Add(time.Minute),
},
},
{
// Alert A has the Timeout flag unset while Alert B has it set.
// StartsAt is defined by Alert A.
// EndsAt is defined by Alert A.
A: &Alert{
Alert: model.Alert{
StartsAt: now.Add(-time.Minute),
EndsAt: now.Add(3 * time.Minute),
},
UpdatedAt: now,
},
B: &Alert{
Alert: model.Alert{
StartsAt: now,
EndsAt: now.Add(2 * time.Minute),
},
UpdatedAt: now.Add(time.Minute),
Timeout: true,
},
Res: &Alert{
Alert: model.Alert{
StartsAt: now.Add(-time.Minute),
Expand All @@ -55,12 +115,148 @@ func TestAlertMerge(t *testing.T) {
Timeout: true,
},
},
{
// Both alerts have the Timeout flag unset and are not resolved.
// StartsAt is defined by Alert A.
// EndsAt is defined by Alert A.
A: &Alert{
Alert: model.Alert{
StartsAt: now.Add(-time.Minute),
EndsAt: now.Add(3 * time.Minute),
},
UpdatedAt: now,
},
B: &Alert{
Alert: model.Alert{
StartsAt: now,
EndsAt: now.Add(2 * time.Minute),
},
UpdatedAt: now.Add(time.Minute),
},
Res: &Alert{
Alert: model.Alert{
StartsAt: now.Add(-time.Minute),
EndsAt: now.Add(3 * time.Minute),
},
UpdatedAt: now.Add(time.Minute),
},
},
{
// Both alerts have the Timeout flag unset and are not resolved.
// StartsAt is defined by Alert A.
// EndsAt is defined by Alert B.
A: &Alert{
Alert: model.Alert{
StartsAt: now.Add(-time.Minute),
EndsAt: now.Add(3 * time.Minute),
},
UpdatedAt: now,
},
B: &Alert{
Alert: model.Alert{
StartsAt: now.Add(-time.Minute),
EndsAt: now.Add(4 * time.Minute),
},
UpdatedAt: now.Add(time.Minute),
},
Res: &Alert{
Alert: model.Alert{
StartsAt: now.Add(-time.Minute),
EndsAt: now.Add(4 * time.Minute),
},
UpdatedAt: now.Add(time.Minute),
},
},
{
// Both alerts have the Timeout flag unset, A is resolved while B isn't.
// StartsAt is defined by Alert A.
// EndsAt is defined by Alert B.
A: &Alert{
Alert: model.Alert{
StartsAt: now.Add(-3 * time.Minute),
EndsAt: now.Add(-time.Minute),
},
UpdatedAt: now,
},
B: &Alert{
Alert: model.Alert{
StartsAt: now.Add(-2 * time.Minute),
EndsAt: now.Add(time.Minute),
},
UpdatedAt: now.Add(time.Minute),
},
Res: &Alert{
Alert: model.Alert{
StartsAt: now.Add(-3 * time.Minute),
EndsAt: now.Add(time.Minute),
},
UpdatedAt: now.Add(time.Minute),
},
},
{
// Both alerts have the Timeout flag unset, B is resolved while A isn't.
// StartsAt is defined by Alert A.
// EndsAt is defined by Alert B.
A: &Alert{
Alert: model.Alert{
StartsAt: now.Add(-2 * time.Minute),
EndsAt: now.Add(3 * time.Minute),
},
UpdatedAt: now,
},
B: &Alert{
Alert: model.Alert{
StartsAt: now.Add(-2 * time.Minute),
EndsAt: now,
},
UpdatedAt: now.Add(time.Minute),
},
Res: &Alert{
Alert: model.Alert{
StartsAt: now.Add(-2 * time.Minute),
EndsAt: now,
Copy link
Member

Choose a reason for hiding this comment

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

I am confused by this. If I have two alerts, where one is firing and one is not, I expect the merged alert to still be firing, right? Especially as none of them have the Timeout flag set.

Copy link
Contributor

Choose a reason for hiding this comment

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

Because B is received after A, it's EndsAt value "wins" because A isn't resolved (is what I'm reading from the code and comments).

Copy link
Member Author

Choose a reason for hiding this comment

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

Because B is received after A

Exactly, an alert that is effectively resolved wins over an older alert that was firing previously.

Copy link
Member

Choose a reason for hiding this comment

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

Right got it. Thanks for clarifying @stuartnelson3 & @simonpasquier!

},
UpdatedAt: now.Add(time.Minute),
},
},
{
// Both alerts are resolved (EndsAt < now).
// StartsAt is defined by Alert B.
// EndsAt is defined by Alert A.
A: &Alert{
Alert: model.Alert{
StartsAt: now.Add(-3 * time.Minute),
EndsAt: now.Add(-time.Minute),
},
UpdatedAt: now.Add(-time.Minute),
},
B: &Alert{
Alert: model.Alert{
StartsAt: now.Add(-4 * time.Minute),
EndsAt: now.Add(-2 * time.Minute),
},
UpdatedAt: now.Add(time.Minute),
},
Res: &Alert{
Alert: model.Alert{
StartsAt: now.Add(-4 * time.Minute),
EndsAt: now.Add(-1 * time.Minute),
},
UpdatedAt: now.Add(time.Minute),
},
},
}

for _, p := range pairs {
if res := p.A.Merge(p.B); !reflect.DeepEqual(p.Res, res) {
t.Errorf("unexpected merged alert %#v", res)
}
for i, p := range pairs {
p := p
t.Run(strconv.Itoa(i), func(t *testing.T) {
if res := p.A.Merge(p.B); !reflect.DeepEqual(p.Res, res) {
t.Errorf("unexpected merged alert %#v", res)
}
if res := p.B.Merge(p.A); !reflect.DeepEqual(p.Res, res) {
t.Errorf("unexpected merged alert %#v", res)
}
})
}
}

Expand Down