Skip to content

Commit

Permalink
Add timezone support to time intervals. (prometheus#2782)
Browse files Browse the repository at this point in the history
* Add explicit UTC to time interval tests

Signed-off-by: Ben Ridley <benridley29@gmail.com>

* Add timezone support to time intervals

Signed-off-by: Ben Ridley <benridley29@gmail.com>

* Update time interval documentation with time zone info

Signed-off-by: Ben Ridley <benridley29@gmail.com>

* Refactor notification tests to test timezone support

Signed-off-by: Ben Ridley <benridley29@gmail.com>

* Make use of Local more clear

Signed-off-by: Ben Ridley <benridley29@gmail.com>

* Fix documentation about timezone support.

Makes it clear that the default is UTC, but others are supported.

Signed-off-by: Ben Ridley <benridley29@gmail.com>

* Remove commented/unused function

Signed-off-by: Ben Ridley <benridley29@gmail.com>

* Fix tests using incorrect timezones

Previously tests were using time zone names that were unsupported by the
RFC822 parser. This switches the tests to use RFC822Z and specifies the
zones by number.

Signed-off-by: Ben Ridley <benridley29@gmail.com>

* Add a few more timezone test cases

Signed-off-by: Ben Ridley <benridley29@gmail.com>

* Remove unnecessary if/else branch

Co-authored-by: Sylvain Rabot <sylvain@abstraction.fr>
Signed-off-by: Ben Ridley <benridley29@gmail.com>

* Rename timezone to location for consistency with Go stdlib

Signed-off-by: Ben Ridley <benridley29@gmail.com>

* Make Windows timezone error more specific

Signed-off-by: Ben Ridley <benridley29@gmail.com>

* Update docs to use 'location'

Signed-off-by: Ben Ridley <benridley29@gmail.com>

* Apply suggestions from code review

Co-authored-by: Sylvain Rabot <sylvain@abstraction.fr>
Signed-off-by: Ben Ridley <benridley29@gmail.com>

Signed-off-by: Ben Ridley <benridley29@gmail.com>
Co-authored-by: Sylvain Rabot <sylvain@abstraction.fr>
Signed-off-by: Yijie Qin <qinyijie@amazon.com>
  • Loading branch information
2 people authored and qinxx108 committed Dec 13, 2022
1 parent a0d8d76 commit 17eedc0
Show file tree
Hide file tree
Showing 4 changed files with 219 additions and 72 deletions.
24 changes: 22 additions & 2 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -285,13 +285,14 @@ supports the following fields:
[ - <month_range> ...]
years:
[ - <year_range> ...]
location: <string>
```

All fields are lists. Within each non-empty list, at least one element must be satisfied to match
the field. If a field is left unspecified, any value will match the field. For an instant of time
to match a complete time interval, all fields must match.
Some fields support ranges and negative indices, and are detailed below. All definitions are
taken to be in UTC, no other timezones are currently supported.
Some fields support ranges and negative indices, and are detailed below. If a time zone is not
specified, then the times are taken to be in UTC.

`time_range`: Ranges inclusive of the starting time and exclusive of the end time to
make it easy to represent times that start/end on hour boundaries.
Expand Down Expand Up @@ -321,6 +322,25 @@ Inclusive on both ends.
`year_range`: A numerical list of years. Ranges are accepted. For example, `['2020:2022', '2030']`.
Inclusive on both ends.

`location`: A string that matches a location in the IANA time zone database. For
example, `'Australia/Sydney'`. The location provides the time zone for the time
interval. For example, a time interval with a location of `'Australia/Sydney'` that
contained something like:

times:
- start_time: 09:00
end_time: 17:00
weekdays: ['monday:friday']

would include any time that fell between the hours of 9:00AM and 5:00PM, between Monday
and Friday, using the local time in Sydney, Australia.

You may also use `'Local'` as a location to use the local time of the machine where
Alertmanager is running, or `'UTC'` for UTC time. If no timezone is provided, the time
interval is taken to be in UTC time.**Note:** On Windows, only `Local` or `UTC` are
supported unless you provide a custom time zone database using the `ZONEINFO`
environment variable.

## `<inhibit_rule>`

An inhibition rule mutes an alert (target) matching a set of matchers
Expand Down
33 changes: 23 additions & 10 deletions notify/notify_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -724,16 +724,20 @@ func TestMuteStageWithSilences(t *testing.T) {
}

func TestTimeMuteStage(t *testing.T) {
// Route mutes alerts outside business hours if it is a mute_time_interval
// Route 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']`
- weekdays: ['saturday', 'sunday']
months: ['November']
location: 'Australia/Sydney'`

cases := []struct {
fireTime string
Expand All @@ -742,40 +746,49 @@ func TestTimeMuteStage(t *testing.T) {
}{
{
// Friday during business hours
fireTime: "01 Jan 21 09:00 +0000",
fireTime: "19 Nov 21 13:00 +1100",
labels: model.LabelSet{"foo": "bar"},
shouldMute: false,
},
{
// Tuesday before 5pm
fireTime: "01 Dec 20 16:59 +0000",
fireTime: "16 Nov 21 16:59 +1100",
labels: model.LabelSet{"dont": "mute"},
shouldMute: false,
},
{
// Saturday
fireTime: "17 Oct 20 10:00 +0000",
fireTime: "20 Nov 21 10:00 +1100",
labels: model.LabelSet{"mute": "me"},
shouldMute: true,
},
{
// Wednesday before 9am
fireTime: "14 Oct 20 05:00 +0000",
fireTime: "17 Nov 21 05:00 +1100",
labels: model.LabelSet{"mute": "me"},
shouldMute: true,
},
{
// Ensure comparisons are UTC only. 12:00 KST should be muted (03:00 UTC)
fireTime: "14 Oct 20 12:00 +0900",
// Ensure comparisons with other time zones work as expected.
fireTime: "14 Nov 21 20:00 +0900",
labels: model.LabelSet{"mute": "kst"},
shouldMute: true,
},
{
// Ensure comparisons are UTC only. 22:00 KST should not be muted (13:00 UTC)
fireTime: "14 Oct 20 22:00 +0900",
fireTime: "14 Nov 21 21:30 +0000",
labels: model.LabelSet{"mute": "utc"},
shouldMute: true,
},
{
fireTime: "15 Nov 22 14:30 +0900",
labels: model.LabelSet{"kst": "dont_mute"},
shouldMute: false,
},
{
fireTime: "15 Nov 21 02:00 -0500",
labels: model.LabelSet{"mute": "0500"},
shouldMute: true,
},
}
var intervals []timeinterval.TimeInterval
err := yaml.Unmarshal([]byte(muteIn), &intervals)
Expand Down
59 changes: 59 additions & 0 deletions timeinterval/timeinterval.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ import (
"encoding/json"
"errors"
"fmt"
"os"
"regexp"
"runtime"
"strconv"
"strings"
"time"
Expand All @@ -33,6 +35,7 @@ type TimeInterval struct {
DaysOfMonth []DayOfMonthRange `yaml:"days_of_month,flow,omitempty" json:"days_of_month,omitempty"`
Months []MonthRange `yaml:"months,flow,omitempty" json:"months,omitempty"`
Years []YearRange `yaml:"years,flow,omitempty" json:"years,omitempty"`
Location *Location `yaml:"location,flow,omitempty" json:"location,omitempty"`
}

// TimeRange represents a range of minutes within a 1440 minute day, exclusive of the End minute. A day consists of 1440 minutes.
Expand Down Expand Up @@ -68,6 +71,11 @@ type YearRange struct {
InclusiveRange
}

// A Location is a container for a time.Location, used for custom unmarshalling/validation logic.
type Location struct {
*time.Location
}

type yamlTimeRange struct {
StartTime string `yaml:"start_time" json:"start_time"`
EndTime string `yaml:"end_time" json:"end_time"`
Expand Down Expand Up @@ -166,6 +174,34 @@ var monthsInv = map[int]string{
12: "december",
}

// UnmarshalYAML implements the Unmarshaller interface for Location.
func (tz *Location) UnmarshalYAML(unmarshal func(interface{}) error) error {
var str string
if err := unmarshal(&str); err != nil {
return err
}

loc, err := time.LoadLocation(str)
if err != nil {
if runtime.GOOS == "windows" {
if zoneinfo := os.Getenv("ZONEINFO"); zoneinfo != "" {
return fmt.Errorf("%w (ZONEINFO=%q)", err, zoneinfo)
}
return fmt.Errorf("%w (on Windows platforms, you may have to pass the time zone database using the ZONEINFO environment variable, see https://pkg.go.dev/time#LoadLocation for details)", err)
}
return err
}

*tz = Location{loc}
return nil
}

// UnmarshalJSON implements the json.Unmarshaler interface for Location.
// It delegates to the YAML unmarshaller as it can parse JSON and has validation logic.
func (tz *Location) UnmarshalJSON(in []byte) error {
return yaml.Unmarshal(in, tz)
}

// UnmarshalYAML implements the Unmarshaller interface for WeekdayRange.
func (r *WeekdayRange) UnmarshalYAML(unmarshal func(interface{}) error) error {
var str string
Expand Down Expand Up @@ -363,6 +399,26 @@ func (tr TimeRange) MarshalJSON() (out []byte, err error) {
return json.Marshal(yTr)
}

// MarshalText implements the econding.TextMarshaler interface for Location.
// It marshals a Location back into a string that represents a time.Location.
func (tz Location) MarshalText() ([]byte, error) {
if tz.Location == nil {
return nil, fmt.Errorf("unable to convert nil location into string")
}
return []byte(tz.Location.String()), nil
}

//MarshalYAML implements the yaml.Marshaler interface for Location.
func (tz Location) MarshalYAML() (interface{}, error) {
bytes, err := tz.MarshalText()
return string(bytes), err
}

//MarshalJSON implements the json.Marshaler interface for Location.
func (tz Location) MarshalJSON() (out []byte, err error) {
return json.Marshal(tz.String())
}

// MarshalText implements the encoding.TextMarshaler interface for InclusiveRange.
// It converts the struct into a colon-separated string, or a single element if
// appropriate. e.g. "monday:friday" or "monday"
Expand Down Expand Up @@ -408,6 +464,9 @@ func clamp(n, min, max int) int {

// ContainsTime returns true if the TimeInterval contains the given time, otherwise returns false.
func (tp TimeInterval) ContainsTime(t time.Time) bool {
if tp.Location != nil {
t = t.In(tp.Location.Location)
}
if tp.Times != nil {
in := false
for _, validMinutes := range tp.Times {
Expand Down

0 comments on commit 17eedc0

Please sign in to comment.