Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#3513: TimeMuter returns the names of time intervals #3791

Merged
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
4 changes: 2 additions & 2 deletions notify/notify.go
Original file line number Diff line number Diff line change
Expand Up @@ -949,7 +949,7 @@ func (tms TimeMuteStage) Exec(ctx context.Context, l log.Logger, alerts ...*type
return ctx, alerts, nil
}

muted, err := tms.muter.Mutes(muteTimeIntervalNames, now)
muted, _, err := tms.muter.Mutes(muteTimeIntervalNames, now)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We do not have GroupMarker (added here) so there is nothing to be marked. Use the blank identifier here instead.

if err != nil {
return ctx, alerts, err
}
Expand Down Expand Up @@ -987,7 +987,7 @@ func (tas TimeActiveStage) Exec(ctx context.Context, l log.Logger, alerts ...*ty
return ctx, alerts, errors.New("missing now timestamp")
}

muted, err := tas.muter.Mutes(activeTimeIntervalNames, now)
muted, _, err := tas.muter.Mutes(activeTimeIntervalNames, now)
if err != nil {
return ctx, alerts, err
}
Expand Down
10 changes: 6 additions & 4 deletions timeinterval/timeinterval.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,21 +33,23 @@ type Intervener struct {
intervals map[string][]TimeInterval
}

func (i *Intervener) Mutes(names []string, now time.Time) (bool, error) {
// Mutes implements the TimeMuter interface.
func (i *Intervener) Mutes(names []string, now time.Time) (bool, []string, error) {
grobinson-grafana marked this conversation as resolved.
Show resolved Hide resolved
var in []string
for _, name := range names {
interval, ok := i.intervals[name]
if !ok {
return false, fmt.Errorf("time interval %s doesn't exist in config", name)
return false, nil, fmt.Errorf("time interval %s doesn't exist in config", name)
}

for _, ti := range interval {
if ti.ContainsTime(now.UTC()) {
return true, nil
in = append(in, name)
}
}
}

return false, nil
return len(in) > 0, in, nil
}

func NewIntervener(ti map[string][]TimeInterval) *Intervener {
Expand Down
160 changes: 78 additions & 82 deletions timeinterval/timeinterval_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package timeinterval
import (
"encoding/json"
"reflect"
"sort"
"testing"
"time"

Expand Down Expand Up @@ -662,95 +663,90 @@ func mustLoadLocation(name string) *time.Location {
}

func TestIntervener_Mutes(t *testing.T) {
// muteIn mutes alerts outside business hours in November, using the +1100 timezone.
muteIn := `
---
- weekdays:
- monday:friday
location: Australia/Sydney
months:
- November
times:
- start_time: 00:00
end_time: 09:00
- start_time: 17:00
end_time: 24:00
- weekdays:
- saturday
- sunday
months:
- November
location: 'Australia/Sydney'
`
intervalName := "test"
var intervals []TimeInterval
err := yaml.Unmarshal([]byte(muteIn), &intervals)
require.NoError(t, err)
m := map[string][]TimeInterval{intervalName: intervals}

tc := []struct {
name string
firedAt string
expected bool
err error
}{
{
name: "Should not mute on Friday during business hours",
firedAt: "19 Nov 21 13:00 +1100",
expected: false,
},
{
name: "Should not mute on a Tuesday before 5pm",
firedAt: "16 Nov 21 16:59 +1100",
expected: false,
},
{
name: "Should mute on a Saturday",
firedAt: "20 Nov 21 10:00 +1100",
expected: true,
},
{
name: "Should mute before 9am on a Wednesday",
firedAt: "17 Nov 21 05:00 +1100",
expected: true,
},
{
name: "Should mute even if we are in a different timezone (KST)",
firedAt: "14 Nov 21 20:00 +0900",
expected: true,
},
{
name: "Should mute even if the timezone is UTC",
firedAt: "14 Nov 21 21:30 +0000",
expected: true,
},
{
name: "Should not mute different timezone (KST)",
firedAt: "15 Nov 22 14:30 +0900",
expected: false,
},
{
name: "Should mute in a different timezone (PET)",
firedAt: "15 Nov 21 02:00 -0500",
expected: true,
},
sydney, err := time.LoadLocation("Australia/Sydney")
if err != nil {
t.Fatalf("Failed to load location Australia/Sydney: %s", err)
}
eveningsAndWeekends := map[string][]TimeInterval{
"evenings": {{
Times: []TimeRange{{
StartMinute: 0, // 00:00
EndMinute: 540, // 09:00
}, {
StartMinute: 1020, // 17:00
EndMinute: 1440, // 24:00
}},
Location: &Location{Location: sydney},
}},
"weekends": {{
Weekdays: []WeekdayRange{{
InclusiveRange: InclusiveRange{Begin: 6, End: 6}, // Saturday
}, {
InclusiveRange: InclusiveRange{Begin: 0, End: 0}, // Sunday
}},
Location: &Location{Location: sydney},
}},
}

for _, tt := range tc {
t.Run(tt.name, func(t *testing.T) {
now, err := time.Parse(time.RFC822Z, tt.firedAt)
require.NoError(t, err)
tests := []struct {
name string
intervals map[string][]TimeInterval
now time.Time
mutedBy []string
}{{
name: "Should be muted outside working hours",
intervals: eveningsAndWeekends,
now: time.Date(2024, 1, 1, 0, 0, 0, 0, sydney),
mutedBy: []string{"evenings"},
}, {
name: "Should not be muted during working hours",
intervals: eveningsAndWeekends,
now: time.Date(2024, 1, 1, 9, 0, 0, 0, sydney),
mutedBy: nil,
}, {
name: "Should be muted during weekends",
intervals: eveningsAndWeekends,
now: time.Date(2024, 1, 6, 10, 0, 0, 0, sydney),
mutedBy: []string{"weekends"},
}, {
name: "Should be muted during weekend evenings",
intervals: eveningsAndWeekends,
now: time.Date(2024, 1, 6, 17, 0, 0, 0, sydney),
mutedBy: []string{"evenings", "weekends"},
}, {
name: "Should be muted at 12pm UTC on a weekday",
intervals: eveningsAndWeekends,
now: time.Date(2024, 1, 1, 10, 0, 0, 0, time.UTC),
mutedBy: []string{"evenings"},
}, {
name: "Should be muted at 12pm UTC on a weekend",
intervals: eveningsAndWeekends,
now: time.Date(2024, 1, 6, 10, 0, 0, 0, time.UTC),
mutedBy: []string{"evenings", "weekends"},
}}

intervener := NewIntervener(m)
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
intervener := NewIntervener(test.intervals)

expected, err := intervener.Mutes([]string{intervalName}, now)
if err != nil {
require.Error(t, tt.err)
require.False(t, tt.expected)
// Get the names of all time intervals for the context.
timeIntervalNames := make([]string, 0, len(test.intervals))
for name := range test.intervals {
timeIntervalNames = append(timeIntervalNames, name)
}
// Sort the names so we can compare mutedBy with test.mutedBy.
sort.Strings(timeIntervalNames)

isMuted, mutedBy, err := intervener.Mutes(timeIntervalNames, test.now)
require.NoError(t, err)
require.Equal(t, expected, tt.expected)

if len(test.mutedBy) == 0 {
require.False(t, isMuted)
require.Empty(t, mutedBy)
} else {
require.True(t, isMuted)
require.Equal(t, test.mutedBy, mutedBy)
}
})
}
}
6 changes: 4 additions & 2 deletions types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -416,9 +416,11 @@ type Muter interface {
Mutes(model.LabelSet) bool
}

// TimeMuter determines if alerts should be muted based on the specified current time and active time interval on the route.
// A TimeMuter determines if the time is muted by one or more active or mute
// time intervals. If the time is muted, it returns true and the names of the
// time intervals that muted it. Otherwise, it returns false and a nil slice.
type TimeMuter interface {
Mutes(timeIntervalName []string, now time.Time) (bool, error)
Mutes(timeIntervalNames []string, now time.Time) (bool, []string, error)
}

// A MuteFunc is a function that implements the Muter interface.
Expand Down
Loading