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

* TimeMuter returns the names of time intervals

This commit updates the TimeMuter interface to also return the names
of the time intervals that muted the alerts.

Signed-off-by: George Robinson <george.robinson@grafana.com>

---------

Signed-off-by: George Robinson <george.robinson@grafana.com>
This commit is contained in:
George Robinson 2024-04-30 12:47:00 +01:00 committed by GitHub
parent a9b5cb4351
commit dacbf0050b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 90 additions and 90 deletions

View File

@ -949,7 +949,7 @@ func (tms TimeMuteStage) Exec(ctx context.Context, l log.Logger, alerts ...*type
return ctx, alerts, nil return ctx, alerts, nil
} }
muted, err := tms.muter.Mutes(muteTimeIntervalNames, now) muted, _, err := tms.muter.Mutes(muteTimeIntervalNames, now)
if err != nil { if err != nil {
return ctx, alerts, err return ctx, alerts, err
} }
@ -987,7 +987,7 @@ func (tas TimeActiveStage) Exec(ctx context.Context, l log.Logger, alerts ...*ty
return ctx, alerts, errors.New("missing now timestamp") 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 { if err != nil {
return ctx, alerts, err return ctx, alerts, err
} }

View File

@ -33,21 +33,23 @@ type Intervener struct {
intervals map[string][]TimeInterval 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) {
var in []string
for _, name := range names { for _, name := range names {
interval, ok := i.intervals[name] interval, ok := i.intervals[name]
if !ok { 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 { for _, ti := range interval {
if ti.ContainsTime(now.UTC()) { 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 { func NewIntervener(ti map[string][]TimeInterval) *Intervener {

View File

@ -16,6 +16,7 @@ package timeinterval
import ( import (
"encoding/json" "encoding/json"
"reflect" "reflect"
"sort"
"testing" "testing"
"time" "time"
@ -662,95 +663,90 @@ func mustLoadLocation(name string) *time.Location {
} }
func TestIntervener_Mutes(t *testing.T) { func TestIntervener_Mutes(t *testing.T) {
// muteIn mutes alerts outside business hours in November, using the +1100 timezone. sydney, err := time.LoadLocation("Australia/Sydney")
muteIn := ` if err != nil {
--- t.Fatalf("Failed to load location Australia/Sydney: %s", err)
- weekdays: }
- monday:friday eveningsAndWeekends := map[string][]TimeInterval{
location: Australia/Sydney "evenings": {{
months: Times: []TimeRange{{
- November StartMinute: 0, // 00:00
times: EndMinute: 540, // 09:00
- start_time: 00:00 }, {
end_time: 09:00 StartMinute: 1020, // 17:00
- start_time: 17:00 EndMinute: 1440, // 24:00
end_time: 24:00 }},
- weekdays: Location: &Location{Location: sydney},
- saturday }},
- sunday "weekends": {{
months: Weekdays: []WeekdayRange{{
- November InclusiveRange: InclusiveRange{Begin: 6, End: 6}, // Saturday
location: 'Australia/Sydney' }, {
` InclusiveRange: InclusiveRange{Begin: 0, End: 0}, // Sunday
intervalName := "test" }},
var intervals []TimeInterval Location: &Location{Location: sydney},
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,
},
} }
for _, tt := range tc { tests := []struct {
t.Run(tt.name, func(t *testing.T) { name string
now, err := time.Parse(time.RFC822Z, tt.firedAt) intervals map[string][]TimeInterval
require.NoError(t, err) 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) // Get the names of all time intervals for the context.
if err != nil { timeIntervalNames := make([]string, 0, len(test.intervals))
require.Error(t, tt.err) for name := range test.intervals {
require.False(t, tt.expected) 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.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)
}
}) })
} }
} }

View File

@ -416,9 +416,11 @@ type Muter interface {
Mutes(model.LabelSet) bool 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 { 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. // A MuteFunc is a function that implements the Muter interface.