From c2c0c5544db4d580057db553d4263a22e74c7703 Mon Sep 17 00:00:00 2001 From: ben Date: Tue, 22 Sep 2020 17:13:10 +1000 Subject: [PATCH 01/47] Add gotime library Signed-off-by: Ben Ridley --- go.mod | 1 + go.sum | 2 + vendor/github.com/benridley/gotime/.gitignore | 15 + vendor/github.com/benridley/gotime/README.md | 15 + vendor/github.com/benridley/gotime/go.mod | 5 + vendor/github.com/benridley/gotime/go.sum | 4 + vendor/github.com/benridley/gotime/gotime.go | 384 ++++++++++++++++++ vendor/modules.txt | 3 + 8 files changed, 429 insertions(+) create mode 100644 vendor/github.com/benridley/gotime/.gitignore create mode 100644 vendor/github.com/benridley/gotime/README.md create mode 100644 vendor/github.com/benridley/gotime/go.mod create mode 100644 vendor/github.com/benridley/gotime/go.sum create mode 100644 vendor/github.com/benridley/gotime/gotime.go diff --git a/go.mod b/go.mod index 09c54bbaf7..bff3401813 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,7 @@ module github.com/prometheus/alertmanager require ( github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d + github.com/benridley/gotime v0.0.1 github.com/cenkalti/backoff/v4 v4.0.2 github.com/cespare/xxhash v1.1.0 github.com/go-kit/kit v0.10.0 diff --git a/go.sum b/go.sum index 422121b57e..b2b320f753 100644 --- a/go.sum +++ b/go.sum @@ -38,6 +38,8 @@ github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:o github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= +github.com/benridley/gotime v0.0.1 h1:wd8RPtHpAev+8FyOT7SoxV5nfTrpu6vBBMooAAdiNLg= +github.com/benridley/gotime v0.0.1/go.mod h1:/leEq6n8vU9CTErhe8vZI5sZPnRMMTP1/YaDzoagvwg= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= diff --git a/vendor/github.com/benridley/gotime/.gitignore b/vendor/github.com/benridley/gotime/.gitignore new file mode 100644 index 0000000000..66fd13c903 --- /dev/null +++ b/vendor/github.com/benridley/gotime/.gitignore @@ -0,0 +1,15 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ diff --git a/vendor/github.com/benridley/gotime/README.md b/vendor/github.com/benridley/gotime/README.md new file mode 100644 index 0000000000..15f14487bb --- /dev/null +++ b/vendor/github.com/benridley/gotime/README.md @@ -0,0 +1,15 @@ +# gotime +A go library for defining windows of time and validating points in time against those periods. + +# How to Use +The main struct, the TimeInterval, is designed to be instantiated by a yaml configuration file: +```yaml + # Last week, excluding Saturday, of the first quarter of the year during business hours from 2020 to 2025 and 2030-2035 +- weekdays: ['monday:friday', 'sunday'] + months: ['january:march'] + days_of_month: ['-7:-1'] + years: ['2020:2025', '2030:2035'] + times: + - start_time: '09:00' + end_time: '17:00' +``` diff --git a/vendor/github.com/benridley/gotime/go.mod b/vendor/github.com/benridley/gotime/go.mod new file mode 100644 index 0000000000..9e7b2ccb2a --- /dev/null +++ b/vendor/github.com/benridley/gotime/go.mod @@ -0,0 +1,5 @@ +module github.com/benridley/gotime + +go 1.14 + +require gopkg.in/yaml.v2 v2.3.0 diff --git a/vendor/github.com/benridley/gotime/go.sum b/vendor/github.com/benridley/gotime/go.sum new file mode 100644 index 0000000000..168980da5f --- /dev/null +++ b/vendor/github.com/benridley/gotime/go.sum @@ -0,0 +1,4 @@ +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/vendor/github.com/benridley/gotime/gotime.go b/vendor/github.com/benridley/gotime/gotime.go new file mode 100644 index 0000000000..79d16ff4d1 --- /dev/null +++ b/vendor/github.com/benridley/gotime/gotime.go @@ -0,0 +1,384 @@ +package gotime + +import ( + "errors" + "fmt" + "regexp" + "strconv" + "strings" + "time" +) + +// TimeInterval describes intervals of time. ContainsTime will tell you if a golang time is contained +// within the interval. +type TimeInterval struct { + Times []timeRange `yaml:"times"` + Weekdays []weekdayRange `yaml:"weekdays"` + DaysOfMonth []dayOfMonthRange `yaml:"days_of_month"` + Months []monthRange `yaml:"months"` + Years []yearRange `yaml:"years"` +} + +/* TimeRange represents a range of minutes within a 1440 minute day, exclusive of the end minute. A day consists of 1440 minutes. + For example, 5:00PM to end of the day would begin at 1020 and end at 1440. */ +type timeRange struct { + startMinute int + endMinute int +} + +// inclusiveRange is used to hold the beginning and end values of many time interval components +type inclusiveRange struct { + begin int + end int +} + +// A weekdayRange is an inclusive range between [0, 6] where 0 = Sunday +type weekdayRange struct { + inclusiveRange +} + +// A dayOfMonthRange is an inclusive range that may have negative beginning/end values that represent distance from the end of the month beginning at -1 +type dayOfMonthRange struct { + inclusiveRange +} + +// A monthRange is an inclusive range between [1, 12] where 1 = January +type monthRange struct { + inclusiveRange +} + +// A year range is a positive inclusive range +type yearRange struct { + inclusiveRange +} + +type yamlTimeRange struct { + StartTime string `yaml:"start_time"` + EndTime string `yaml:"end_time"` +} + +// A range with a beginning and end that can be represented as strings +type stringableRange interface { + setBegin(int) + setEnd(int) + // Try to map a member of the range into an integer. + memberFromString(string) (int, error) +} + +func (ir *inclusiveRange) setBegin(n int) { + ir.begin = n +} + +func (ir *inclusiveRange) setEnd(n int) { + ir.end = n +} + +func (ir *inclusiveRange) memberFromString(in string) (out int, err error) { + out, err = strconv.Atoi(in) + if err != nil { + return -1, err + } + return out, nil +} + +func (r *weekdayRange) memberFromString(in string) (out int, err error) { + out, ok := daysOfWeek[in] + if !ok { + return -1, fmt.Errorf("%s is not a valid weekday", in) + } + return out, nil +} + +func (r *monthRange) memberFromString(in string) (out int, err error) { + out, ok := months[in] + if !ok { + return -1, fmt.Errorf("%s is not a valid weekday", in) + } + return out, nil +} + +var daysOfWeek = map[string]int{ + "sunday": 0, + "monday": 1, + "tuesday": 2, + "wednesday": 3, + "thursday": 4, + "friday": 5, + "saturday": 6, +} + +var months = map[string]int{ + "january": 1, + "february": 2, + "march": 3, + "april": 4, + "may": 5, + "june": 6, + "july": 7, + "august": 8, + "september": 9, + "october": 10, + "november": 11, + "december": 12, +} + +func (r *weekdayRange) UnmarshalYAML(unmarshal func(interface{}) error) error { + var str string + if err := unmarshal(&str); err != nil { + return err + } + err := stringableRangeFromString(str, r) + if r.begin > r.end { + return errors.New("Start day cannot be before end day") + } + if r.begin < 0 || r.begin > 6 { + return fmt.Errorf("%s is not a valid day of the week: out of range", str) + } + if r.end < 0 || r.end > 6 { + return fmt.Errorf("%s is not a valid day of the week: out of range", str) + } + return err +} + +func (r *dayOfMonthRange) UnmarshalYAML(unmarshal func(interface{}) error) error { + var str string + if err := unmarshal(&str); err != nil { + return err + } + err := stringableRangeFromString(str, r) + if r.begin == 0 || r.begin < -31 || r.begin > 31 { + return fmt.Errorf("%d is not a valid day of the month: out of range", r.begin) + } + if r.end == 0 || r.end < -31 || r.end > 31 { + return fmt.Errorf("%d is not a valid day of the month: out of range", r.end) + } + // Check beginning <= end accounting for negatives day of month indices + trueBegin := r.begin + trueEnd := r.end + if r.begin < 0 { + trueBegin = 30 + r.begin + } + if r.end < 0 { + trueEnd = 30 + r.end + } + if trueBegin > trueEnd { + return errors.New("Start day cannot be before end day") + } + return err +} + +func (r *monthRange) UnmarshalYAML(unmarshal func(interface{}) error) error { + var str string + if err := unmarshal(&str); err != nil { + return err + } + err := stringableRangeFromString(str, r) + if r.begin > r.end { + return errors.New("Start month cannot be before end month") + } + if r.begin < 1 || r.begin > 12 { + return fmt.Errorf("%s is not a valid month: out of range", str) + } + if r.end < 1 || r.end > 12 { + return fmt.Errorf("%s is not a valid month: out of range", str) + } + return err +} + +func (r *yearRange) UnmarshalYAML(unmarshal func(interface{}) error) error { + var str string + if err := unmarshal(&str); err != nil { + return err + } + err := stringableRangeFromString(str, r) + if r.begin > r.end { + return errors.New("Start day cannot be before end day") + } + return err +} + +// UnmarshalYAML implements the Unmarshaller interface for timeRanges. +func (tr *timeRange) UnmarshalYAML(unmarshal func(interface{}) error) error { + var y yamlTimeRange + if err := unmarshal(&y); err != nil { + return err + } + if y.EndTime == "" || y.StartTime == "" { + return errors.New("Both start and end times must be provided") + } + start, err := parseTime(y.StartTime) + if err != nil { + return nil + } + end, err := parseTime(y.EndTime) + if err != nil { + return err + } + if start < 0 { + return errors.New("Start time out of range") + } + if end > 1440 { + return errors.New("End time out of range") + } + if start >= end { + return errors.New("Start time cannot be equal or greater than end time") + } + tr.startMinute, tr.endMinute = start, end + return nil +} + +// TimeLayout specifies the layout to be used in time.Parse() calls for time intervals +const TimeLayout = "15:04" + +var validTime string = "^((([01][0-9])|(2[0-3])):[0-5][0-9])$|(^24:00$)" +var validTimeRE *regexp.Regexp = regexp.MustCompile(validTime) + +// Given a time, determines the number of days in the month that time occurs in. +func daysInMonth(t time.Time) int { + monthStart := time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, t.Location()) + monthEnd := monthStart.AddDate(0, 1, 0) + diff := monthEnd.Sub(monthStart) + return int(diff.Hours() / 24) +} + +func clamp(n, min, max int) int { + if n <= min { + return min + } + if n >= max { + return max + } + return n +} + +// ContainsTime returns true if the TimeInterval contains the given time, otherwise returns false +func (tp TimeInterval) ContainsTime(t time.Time) bool { + if tp.Times != nil { + in := false + for _, validMinutes := range tp.Times { + if (t.Hour()*60+t.Minute()) >= validMinutes.startMinute && (t.Hour()*60+t.Minute()) < validMinutes.endMinute { + in = true + break + } + } + if !in { + return false + } + } + if tp.DaysOfMonth != nil { + in := false + for _, validDates := range tp.DaysOfMonth { + var begin, end int + daysInMonth := daysInMonth(t) + if validDates.begin < 0 { + begin = daysInMonth + validDates.begin + 1 + } else { + begin = validDates.begin + } + if validDates.end < 0 { + end = daysInMonth + validDates.end + 1 + } else { + end = validDates.end + } + // Clamp to the boundaries of the month to prevent crossing into other months + begin = clamp(begin, -1*daysInMonth, daysInMonth) + end = clamp(end, -1*daysInMonth, daysInMonth) + if t.Day() >= begin && t.Day() <= end { + in = true + break + } + } + if !in { + return false + } + } + if tp.Months != nil { + in := false + for _, validMonths := range tp.Months { + if t.Month() >= time.Month(validMonths.begin) && t.Month() <= time.Month(validMonths.end) { + in = true + break + } + } + if !in { + return false + } + } + if tp.Weekdays != nil { + in := false + for _, validDays := range tp.Weekdays { + if t.Weekday() >= time.Weekday(validDays.begin) && t.Weekday() <= time.Weekday(validDays.end) { + in = true + break + } + } + if !in { + return false + } + } + if tp.Years != nil { + in := false + for _, validYears := range tp.Years { + if t.Year() >= validYears.begin && t.Year() <= validYears.end { + in = true + break + } + } + if !in { + return false + } + } + return true +} + +func parseTime(in string) (mins int, err error) { + if !validTimeRE.MatchString(in) { + return 0, fmt.Errorf("Couldn't parse timestamp %s, invalid format", in) + } + timestampComponents := strings.Split(in, ":") + if len(timestampComponents) != 2 { + return 0, fmt.Errorf("Invalid timestamp format: %s", in) + } + timeStampHours, err := strconv.Atoi(timestampComponents[0]) + if err != nil { + return 0, err + } + timeStampMinutes, err := strconv.Atoi(timestampComponents[1]) + if err != nil { + return 0, err + } + if timeStampHours < 0 || timeStampHours > 24 || timeStampMinutes < 0 || timeStampMinutes > 60 { + return 0, fmt.Errorf("Timestamp %s out of range", in) + } + // Timestamps are stored as minutes elapsed in the day, so multiply hours by 60 + mins = timeStampHours*60 + timeStampMinutes + return mins, nil +} + +func stringableRangeFromString(in string, r stringableRange) (err error) { + in = strings.ToLower(in) + if strings.ContainsRune(in, ':') { + components := strings.Split(in, ":") + if len(components) != 2 { + return fmt.Errorf("Coudn't parse range %s, invalid format", in) + } + start, err := r.memberFromString(components[0]) + if err != nil { + return err + } + end, err := r.memberFromString(components[1]) + if err != nil { + return err + } + r.setBegin(start) + r.setEnd(end) + return nil + } + val, err := r.memberFromString(in) + if err != nil { + return err + } + r.setBegin(val) + r.setEnd(val) + return nil +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 4c06197203..74156b5447 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -12,6 +12,9 @@ github.com/alecthomas/units github.com/armon/go-metrics # github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496 github.com/asaskevich/govalidator +# github.com/benridley/gotime v0.0.1 +## explicit +github.com/benridley/gotime # github.com/beorn7/perks v1.0.1 github.com/beorn7/perks/quantile # github.com/cenkalti/backoff/v4 v4.0.2 From 5df661653cd010ece30efb327aee3e91057e26ab Mon Sep 17 00:00:00 2001 From: ben Date: Tue, 22 Sep 2020 17:13:27 +1000 Subject: [PATCH 02/47] Allow time intervals in global config Signed-off-by: Ben Ridley --- config/config.go | 76 +++++++++++++++++++++++++++--------------------- 1 file changed, 43 insertions(+), 33 deletions(-) diff --git a/config/config.go b/config/config.go index c1a873cc40..08473e928d 100644 --- a/config/config.go +++ b/config/config.go @@ -25,6 +25,7 @@ import ( "strings" "time" + "github.com/benridley/gotime" "github.com/pkg/errors" commoncfg "github.com/prometheus/common/config" "github.com/prometheus/common/model" @@ -219,6 +220,12 @@ func resolveFilepaths(baseDir string, cfg *Config) { } } +// A MuteTimeInterval represents a named set of time intervals for which a route should be muted +type MuteTimeInterval struct { + Name string `yaml:"name" json:"name"` + TimeIntervals []gotime.TimeInterval `yaml:"time_intervals"` +} + // Config is the top-level configuration for Alertmanager's config files. type Config struct { Global *GlobalConfig `yaml:"global,omitempty" json:"global,omitempty"` @@ -411,6 +418,9 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error { if len(c.Route.Match) > 0 || len(c.Route.MatchRE) > 0 { return fmt.Errorf("root route must not have any matchers") } + if len(c.Route.MuteTimes) > 0 { + return fmt.Errorf("root route cannot have any mute times") + } // Validate that all receivers used in the routing tree are defined. return checkReceiver(c.Route, names) @@ -436,15 +446,15 @@ func checkReceiver(r *Route, receivers map[string]struct{}) error { // DefaultGlobalConfig returns GlobalConfig with default values. func DefaultGlobalConfig() GlobalConfig { return GlobalConfig{ - ResolveTimeout: model.Duration(5 * time.Minute), - HTTPConfig: &commoncfg.HTTPClientConfig{}, - - SMTPHello: "localhost", - SMTPRequireTLS: true, - PagerdutyURL: mustParseURL("https://events.pagerduty.com/v2/enqueue"), - OpsGenieAPIURL: mustParseURL("https://api.opsgenie.com/"), - WeChatAPIURL: mustParseURL("https://qyapi.weixin.qq.com/cgi-bin/"), - VictorOpsAPIURL: mustParseURL("https://alert.victorops.com/integrations/generic/20131114/alert/"), + ResolveTimeout: model.Duration(5 * time.Minute), + HTTPConfig: &commoncfg.HTTPClientConfig{}, + MuteTimeIntervals: []MuteTimeInterval{}, + SMTPHello: "localhost", + SMTPRequireTLS: true, + PagerdutyURL: mustParseURL("https://events.pagerduty.com/v2/enqueue"), + OpsGenieAPIURL: mustParseURL("https://api.opsgenie.com/"), + WeChatAPIURL: mustParseURL("https://qyapi.weixin.qq.com/cgi-bin/"), + VictorOpsAPIURL: mustParseURL("https://alert.victorops.com/integrations/generic/20131114/alert/"), } } @@ -546,23 +556,24 @@ type GlobalConfig struct { HTTPConfig *commoncfg.HTTPClientConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"` - SMTPFrom string `yaml:"smtp_from,omitempty" json:"smtp_from,omitempty"` - SMTPHello string `yaml:"smtp_hello,omitempty" json:"smtp_hello,omitempty"` - SMTPSmarthost HostPort `yaml:"smtp_smarthost,omitempty" json:"smtp_smarthost,omitempty"` - SMTPAuthUsername string `yaml:"smtp_auth_username,omitempty" json:"smtp_auth_username,omitempty"` - SMTPAuthPassword Secret `yaml:"smtp_auth_password,omitempty" json:"smtp_auth_password,omitempty"` - SMTPAuthSecret Secret `yaml:"smtp_auth_secret,omitempty" json:"smtp_auth_secret,omitempty"` - SMTPAuthIdentity string `yaml:"smtp_auth_identity,omitempty" json:"smtp_auth_identity,omitempty"` - SMTPRequireTLS bool `yaml:"smtp_require_tls" json:"smtp_require_tls,omitempty"` - SlackAPIURL *SecretURL `yaml:"slack_api_url,omitempty" json:"slack_api_url,omitempty"` - PagerdutyURL *URL `yaml:"pagerduty_url,omitempty" json:"pagerduty_url,omitempty"` - OpsGenieAPIURL *URL `yaml:"opsgenie_api_url,omitempty" json:"opsgenie_api_url,omitempty"` - OpsGenieAPIKey Secret `yaml:"opsgenie_api_key,omitempty" json:"opsgenie_api_key,omitempty"` - WeChatAPIURL *URL `yaml:"wechat_api_url,omitempty" json:"wechat_api_url,omitempty"` - WeChatAPISecret Secret `yaml:"wechat_api_secret,omitempty" json:"wechat_api_secret,omitempty"` - WeChatAPICorpID string `yaml:"wechat_api_corp_id,omitempty" json:"wechat_api_corp_id,omitempty"` - VictorOpsAPIURL *URL `yaml:"victorops_api_url,omitempty" json:"victorops_api_url,omitempty"` - VictorOpsAPIKey Secret `yaml:"victorops_api_key,omitempty" json:"victorops_api_key,omitempty"` + SMTPFrom string `yaml:"smtp_from,omitempty" json:"smtp_from,omitempty"` + SMTPHello string `yaml:"smtp_hello,omitempty" json:"smtp_hello,omitempty"` + SMTPSmarthost HostPort `yaml:"smtp_smarthost,omitempty" json:"smtp_smarthost,omitempty"` + SMTPAuthUsername string `yaml:"smtp_auth_username,omitempty" json:"smtp_auth_username,omitempty"` + SMTPAuthPassword Secret `yaml:"smtp_auth_password,omitempty" json:"smtp_auth_password,omitempty"` + SMTPAuthSecret Secret `yaml:"smtp_auth_secret,omitempty" json:"smtp_auth_secret,omitempty"` + SMTPAuthIdentity string `yaml:"smtp_auth_identity,omitempty" json:"smtp_auth_identity,omitempty"` + SMTPRequireTLS bool `yaml:"smtp_require_tls" json:"smtp_require_tls,omitempty"` + SlackAPIURL *SecretURL `yaml:"slack_api_url,omitempty" json:"slack_api_url,omitempty"` + MuteTimeIntervals []MuteTimeInterval `yaml:"mute_time_intervals,omitempty" json:"mute_time_intervals,omitempty"` + PagerdutyURL *URL `yaml:"pagerduty_url,omitempty" json:"pagerduty_url,omitempty"` + OpsGenieAPIURL *URL `yaml:"opsgenie_api_url,omitempty" json:"opsgenie_api_url,omitempty"` + OpsGenieAPIKey Secret `yaml:"opsgenie_api_key,omitempty" json:"opsgenie_api_key,omitempty"` + WeChatAPIURL *URL `yaml:"wechat_api_url,omitempty" json:"wechat_api_url,omitempty"` + WeChatAPISecret Secret `yaml:"wechat_api_secret,omitempty" json:"wechat_api_secret,omitempty"` + WeChatAPICorpID string `yaml:"wechat_api_corp_id,omitempty" json:"wechat_api_corp_id,omitempty"` + VictorOpsAPIURL *URL `yaml:"victorops_api_url,omitempty" json:"victorops_api_url,omitempty"` + VictorOpsAPIKey Secret `yaml:"victorops_api_key,omitempty" json:"victorops_api_key,omitempty"` } // UnmarshalYAML implements the yaml.Unmarshaler interface for GlobalConfig. @@ -579,13 +590,12 @@ type Route struct { GroupByStr []string `yaml:"group_by,omitempty" json:"group_by,omitempty"` GroupBy []model.LabelName `yaml:"-" json:"-"` GroupByAll bool `yaml:"-" json:"-"` - // Deprecated. Remove before v1.0 release. - Match map[string]string `yaml:"match,omitempty" json:"match,omitempty"` - // Deprecated. Remove before v1.0 release. - MatchRE MatchRegexps `yaml:"match_re,omitempty" json:"match_re,omitempty"` - Matchers Matchers `yaml:"matchers,omitempty" json:"matchers,omitempty"` - Continue bool `yaml:"continue" json:"continue,omitempty"` - Routes []*Route `yaml:"routes,omitempty" json:"routes,omitempty"` + + Match map[string]string `yaml:"match,omitempty" json:"match,omitempty"` + MatchRE MatchRegexps `yaml:"match_re,omitempty" json:"match_re,omitempty"` + MuteTimes []string `yaml:"mute_times,omitempty" json:"mute_times,omitempty"` + Continue bool `yaml:"continue" json:"continue,omitempty"` + Routes []*Route `yaml:"routes,omitempty" json:"routes,omitempty"` GroupWait *model.Duration `yaml:"group_wait,omitempty" json:"group_wait,omitempty"` GroupInterval *model.Duration `yaml:"group_interval,omitempty" json:"group_interval,omitempty"` From cbfbf07188b4df8983cbc4be311e4da2521e49d2 Mon Sep 17 00:00:00 2001 From: ben Date: Tue, 22 Sep 2020 17:14:12 +1000 Subject: [PATCH 03/47] Allow routes to reference time intervals Signed-off-by: Ben Ridley --- dispatch/route.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/dispatch/route.go b/dispatch/route.go index bb6477ddaa..bcb3f5aec7 100644 --- a/dispatch/route.go +++ b/dispatch/route.go @@ -34,6 +34,7 @@ var DefaultRouteOpts = RouteOpts{ RepeatInterval: 4 * time.Hour, GroupBy: map[model.LabelName]struct{}{}, GroupByAll: false, + MuteTimes: []string{}, } // A Route is a node that contains definitions of how to handle alerts. @@ -65,6 +66,7 @@ func NewRoute(cr *config.Route, parent *Route) *Route { if cr.Receiver != "" { opts.Receiver = cr.Receiver } + if cr.GroupBy != nil { opts.GroupBy = map[model.LabelName]struct{}{} for _, ln := range cr.GroupBy { @@ -115,6 +117,8 @@ func NewRoute(cr *config.Route, parent *Route) *Route { sort.Sort(matchers) + opts.MuteTimes = cr.MuteTimes + route := &Route{ parent: parent, RouteOpts: opts, @@ -203,6 +207,9 @@ type RouteOpts struct { GroupWait time.Duration GroupInterval time.Duration RepeatInterval time.Duration + + // A list of time intervals for which the route is muted + MuteTimes []string } func (ro *RouteOpts) String() string { From 1d912faab60bafe3d3dbb802ac7e5ea9a094b58f Mon Sep 17 00:00:00 2001 From: ben Date: Sun, 27 Sep 2020 16:07:47 +1000 Subject: [PATCH 04/47] Add config timeinterval that allows intervals to be dumped back out on the status page of Alertmanager Signed-off-by: Ben Ridley --- .gitignore | 1 + config/config.go | 16 ++++++++++++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index cb77717880..40c6a42903 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /data/ +make_container.sh /alertmanager /amtool *.yml diff --git a/config/config.go b/config/config.go index 08473e928d..cc53c7cf42 100644 --- a/config/config.go +++ b/config/config.go @@ -25,7 +25,6 @@ import ( "strings" "time" - "github.com/benridley/gotime" "github.com/pkg/errors" commoncfg "github.com/prometheus/common/config" "github.com/prometheus/common/model" @@ -220,10 +219,19 @@ func resolveFilepaths(baseDir string, cfg *Config) { } } -// A MuteTimeInterval represents a named set of time intervals for which a route should be muted +// A MuteTimeInterval represents a named set of time intervals for which a route should be muted. type MuteTimeInterval struct { - Name string `yaml:"name" json:"name"` - TimeIntervals []gotime.TimeInterval `yaml:"time_intervals"` + Name string `yaml:"name" json:"name"` + TimeIntervals []TimeInterval `yaml:"time_intervals"` +} + +// A TimeInterval describes intervals of time. +type TimeInterval struct { + Times []string `yaml:"times"` + Weekdays []string `yaml:"weekdays"` + DaysOfMonth []string `yaml:"days_of_month"` + Months []string `yaml:"months"` + Years []string `yaml:"years"` } // Config is the top-level configuration for Alertmanager's config files. From ea5b92514745a62b7c1edbf7673de74e9560d4d2 Mon Sep 17 00:00:00 2001 From: ben Date: Sun, 27 Sep 2020 17:52:23 +1000 Subject: [PATCH 05/47] Add buildah script for test container Signed-off-by: Ben Ridley --- .gitignore | 1 - make_container.sh | 7 +++++++ notes.txt | 2 ++ 3 files changed, 9 insertions(+), 1 deletion(-) create mode 100755 make_container.sh create mode 100644 notes.txt diff --git a/.gitignore b/.gitignore index 40c6a42903..cb77717880 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ /data/ -make_container.sh /alertmanager /amtool *.yml diff --git a/make_container.sh b/make_container.sh new file mode 100755 index 0000000000..8155d6e92b --- /dev/null +++ b/make_container.sh @@ -0,0 +1,7 @@ +#!/bin/sh +container=$(buildah from prom/alertmanager) +mnt=$(buildah mount $container) +CGO_ENABLED=0 go build -ldflags="-X 'main.Version=benny'" -o alertmanager cmd/alertmanager/main.go +cp -f ./alertmanager "${mnt}/bin/alertmanager" +buildah commit $container ben-am +buildah unmount $container \ No newline at end of file diff --git a/notes.txt b/notes.txt new file mode 100644 index 0000000000..ad57096a01 --- /dev/null +++ b/notes.txt @@ -0,0 +1,2 @@ +- Unmarshalling is weird. Alertmanager has a status page that just marshalls config into a string. When I tried to use my library with it, it blew up because the timeinterval library hides many of its internal fields and thus they can't be marshalled. + I worked around this by creating a 'config.TimeInterval' concept. Real timeintervals can be created from this representation by re-unmarshalling maybe? From fe4b8399c38292d4276bb5e19edb7d11d5ecaf4a Mon Sep 17 00:00:00 2001 From: ben Date: Tue, 6 Oct 2020 14:48:16 +1100 Subject: [PATCH 06/47] Move time intervals to own section, add config validation Signed-off-by: Ben Ridley --- config/config.go | 104 +++++++++++++++++++++++++++-------------------- 1 file changed, 60 insertions(+), 44 deletions(-) diff --git a/config/config.go b/config/config.go index cc53c7cf42..e17025e9ed 100644 --- a/config/config.go +++ b/config/config.go @@ -25,6 +25,7 @@ import ( "strings" "time" + "github.com/benridley/gotime" "github.com/pkg/errors" commoncfg "github.com/prometheus/common/config" "github.com/prometheus/common/model" @@ -221,26 +222,18 @@ func resolveFilepaths(baseDir string, cfg *Config) { // A MuteTimeInterval represents a named set of time intervals for which a route should be muted. type MuteTimeInterval struct { - Name string `yaml:"name" json:"name"` - TimeIntervals []TimeInterval `yaml:"time_intervals"` -} - -// A TimeInterval describes intervals of time. -type TimeInterval struct { - Times []string `yaml:"times"` - Weekdays []string `yaml:"weekdays"` - DaysOfMonth []string `yaml:"days_of_month"` - Months []string `yaml:"months"` - Years []string `yaml:"years"` + Name string `yaml:"name" json:"name"` + TimeIntervals []gotime.TimeInterval `yaml:"time_intervals"` } // Config is the top-level configuration for Alertmanager's config files. type Config struct { - Global *GlobalConfig `yaml:"global,omitempty" json:"global,omitempty"` - Route *Route `yaml:"route,omitempty" json:"route,omitempty"` - InhibitRules []*InhibitRule `yaml:"inhibit_rules,omitempty" json:"inhibit_rules,omitempty"` - Receivers []*Receiver `yaml:"receivers,omitempty" json:"receivers,omitempty"` - Templates []string `yaml:"templates" json:"templates"` + Global *GlobalConfig `yaml:"global,omitempty" json:"global,omitempty"` + Route *Route `yaml:"route,omitempty" json:"route,omitempty"` + InhibitRules []*InhibitRule `yaml:"inhibit_rules,omitempty" json:"inhibit_rules,omitempty"` + Receivers []*Receiver `yaml:"receivers,omitempty" json:"receivers,omitempty"` + Templates []string `yaml:"templates" json:"templates"` + MuteTimeIntervals []MuteTimeInterval `yaml:"mute_time_intervals,omitempty" json:"mute_time_intervals,omitempty"` // original is the input from which the config was parsed. original string @@ -431,7 +424,15 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error { } // Validate that all receivers used in the routing tree are defined. - return checkReceiver(c.Route, names) + if err := checkReceiver(c.Route, names); err != nil { + return err + } + + tiNames := make(map[string]struct{}) + for _, mt := range c.MuteTimeIntervals { + tiNames[mt.Name] = struct{}{} + } + return checkTimeInterval(c.Route, tiNames) } // checkReceiver returns an error if a node in the routing tree @@ -451,18 +452,34 @@ func checkReceiver(r *Route, receivers map[string]struct{}) error { return nil } +func checkTimeInterval(r *Route, timeIntervals map[string]struct{}) error { + for _, sr := range r.Routes { + if err := checkTimeInterval(sr, timeIntervals); err != nil { + return err + } + } + if len(r.MuteTimes) == 0 { + return nil + } + for _, mt := range r.MuteTimes { + if _, ok := timeIntervals[mt]; !ok { + return fmt.Errorf("undefined time interval %s used in route", mt) + } + } + return nil +} + // DefaultGlobalConfig returns GlobalConfig with default values. func DefaultGlobalConfig() GlobalConfig { return GlobalConfig{ - ResolveTimeout: model.Duration(5 * time.Minute), - HTTPConfig: &commoncfg.HTTPClientConfig{}, - MuteTimeIntervals: []MuteTimeInterval{}, - SMTPHello: "localhost", - SMTPRequireTLS: true, - PagerdutyURL: mustParseURL("https://events.pagerduty.com/v2/enqueue"), - OpsGenieAPIURL: mustParseURL("https://api.opsgenie.com/"), - WeChatAPIURL: mustParseURL("https://qyapi.weixin.qq.com/cgi-bin/"), - VictorOpsAPIURL: mustParseURL("https://alert.victorops.com/integrations/generic/20131114/alert/"), + ResolveTimeout: model.Duration(5 * time.Minute), + HTTPConfig: &commoncfg.HTTPClientConfig{}, + SMTPHello: "localhost", + SMTPRequireTLS: true, + PagerdutyURL: mustParseURL("https://events.pagerduty.com/v2/enqueue"), + OpsGenieAPIURL: mustParseURL("https://api.opsgenie.com/"), + WeChatAPIURL: mustParseURL("https://qyapi.weixin.qq.com/cgi-bin/"), + VictorOpsAPIURL: mustParseURL("https://alert.victorops.com/integrations/generic/20131114/alert/"), } } @@ -564,24 +581,23 @@ type GlobalConfig struct { HTTPConfig *commoncfg.HTTPClientConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"` - SMTPFrom string `yaml:"smtp_from,omitempty" json:"smtp_from,omitempty"` - SMTPHello string `yaml:"smtp_hello,omitempty" json:"smtp_hello,omitempty"` - SMTPSmarthost HostPort `yaml:"smtp_smarthost,omitempty" json:"smtp_smarthost,omitempty"` - SMTPAuthUsername string `yaml:"smtp_auth_username,omitempty" json:"smtp_auth_username,omitempty"` - SMTPAuthPassword Secret `yaml:"smtp_auth_password,omitempty" json:"smtp_auth_password,omitempty"` - SMTPAuthSecret Secret `yaml:"smtp_auth_secret,omitempty" json:"smtp_auth_secret,omitempty"` - SMTPAuthIdentity string `yaml:"smtp_auth_identity,omitempty" json:"smtp_auth_identity,omitempty"` - SMTPRequireTLS bool `yaml:"smtp_require_tls" json:"smtp_require_tls,omitempty"` - SlackAPIURL *SecretURL `yaml:"slack_api_url,omitempty" json:"slack_api_url,omitempty"` - MuteTimeIntervals []MuteTimeInterval `yaml:"mute_time_intervals,omitempty" json:"mute_time_intervals,omitempty"` - PagerdutyURL *URL `yaml:"pagerduty_url,omitempty" json:"pagerduty_url,omitempty"` - OpsGenieAPIURL *URL `yaml:"opsgenie_api_url,omitempty" json:"opsgenie_api_url,omitempty"` - OpsGenieAPIKey Secret `yaml:"opsgenie_api_key,omitempty" json:"opsgenie_api_key,omitempty"` - WeChatAPIURL *URL `yaml:"wechat_api_url,omitempty" json:"wechat_api_url,omitempty"` - WeChatAPISecret Secret `yaml:"wechat_api_secret,omitempty" json:"wechat_api_secret,omitempty"` - WeChatAPICorpID string `yaml:"wechat_api_corp_id,omitempty" json:"wechat_api_corp_id,omitempty"` - VictorOpsAPIURL *URL `yaml:"victorops_api_url,omitempty" json:"victorops_api_url,omitempty"` - VictorOpsAPIKey Secret `yaml:"victorops_api_key,omitempty" json:"victorops_api_key,omitempty"` + SMTPFrom string `yaml:"smtp_from,omitempty" json:"smtp_from,omitempty"` + SMTPHello string `yaml:"smtp_hello,omitempty" json:"smtp_hello,omitempty"` + SMTPSmarthost HostPort `yaml:"smtp_smarthost,omitempty" json:"smtp_smarthost,omitempty"` + SMTPAuthUsername string `yaml:"smtp_auth_username,omitempty" json:"smtp_auth_username,omitempty"` + SMTPAuthPassword Secret `yaml:"smtp_auth_password,omitempty" json:"smtp_auth_password,omitempty"` + SMTPAuthSecret Secret `yaml:"smtp_auth_secret,omitempty" json:"smtp_auth_secret,omitempty"` + SMTPAuthIdentity string `yaml:"smtp_auth_identity,omitempty" json:"smtp_auth_identity,omitempty"` + SMTPRequireTLS bool `yaml:"smtp_require_tls" json:"smtp_require_tls,omitempty"` + SlackAPIURL *SecretURL `yaml:"slack_api_url,omitempty" json:"slack_api_url,omitempty"` + PagerdutyURL *URL `yaml:"pagerduty_url,omitempty" json:"pagerduty_url,omitempty"` + OpsGenieAPIURL *URL `yaml:"opsgenie_api_url,omitempty" json:"opsgenie_api_url,omitempty"` + OpsGenieAPIKey Secret `yaml:"opsgenie_api_key,omitempty" json:"opsgenie_api_key,omitempty"` + WeChatAPIURL *URL `yaml:"wechat_api_url,omitempty" json:"wechat_api_url,omitempty"` + WeChatAPISecret Secret `yaml:"wechat_api_secret,omitempty" json:"wechat_api_secret,omitempty"` + WeChatAPICorpID string `yaml:"wechat_api_corp_id,omitempty" json:"wechat_api_corp_id,omitempty"` + VictorOpsAPIURL *URL `yaml:"victorops_api_url,omitempty" json:"victorops_api_url,omitempty"` + VictorOpsAPIKey Secret `yaml:"victorops_api_key,omitempty" json:"victorops_api_key,omitempty"` } // UnmarshalYAML implements the yaml.Unmarshaler interface for GlobalConfig. From 44d8cb43d2a9dad06953a6c08ca02b9f43e1ee85 Mon Sep 17 00:00:00 2001 From: ben Date: Tue, 6 Oct 2020 21:07:08 +1100 Subject: [PATCH 07/47] Update gotime to v0.0.2 Signed-off-by: Ben Ridley --- go.mod | 2 +- go.sum | 2 + vendor/github.com/benridley/gotime/gotime.go | 260 +++++++++++++------ vendor/modules.txt | 2 +- 4 files changed, 179 insertions(+), 87 deletions(-) diff --git a/go.mod b/go.mod index bff3401813..7123dba512 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,7 @@ module github.com/prometheus/alertmanager require ( github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d - github.com/benridley/gotime v0.0.1 + github.com/benridley/gotime v0.0.2 github.com/cenkalti/backoff/v4 v4.0.2 github.com/cespare/xxhash v1.1.0 github.com/go-kit/kit v0.10.0 diff --git a/go.sum b/go.sum index b2b320f753..48286e5537 100644 --- a/go.sum +++ b/go.sum @@ -40,6 +40,8 @@ github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= github.com/benridley/gotime v0.0.1 h1:wd8RPtHpAev+8FyOT7SoxV5nfTrpu6vBBMooAAdiNLg= github.com/benridley/gotime v0.0.1/go.mod h1:/leEq6n8vU9CTErhe8vZI5sZPnRMMTP1/YaDzoagvwg= +github.com/benridley/gotime v0.0.2 h1:n9OEcwxr57tXcqhoWTm3uDClVzttDNcCydiMNXyGqq0= +github.com/benridley/gotime v0.0.2/go.mod h1:/leEq6n8vU9CTErhe8vZI5sZPnRMMTP1/YaDzoagvwg= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= diff --git a/vendor/github.com/benridley/gotime/gotime.go b/vendor/github.com/benridley/gotime/gotime.go index 79d16ff4d1..de2f399f18 100644 --- a/vendor/github.com/benridley/gotime/gotime.go +++ b/vendor/github.com/benridley/gotime/gotime.go @@ -12,44 +12,44 @@ import ( // TimeInterval describes intervals of time. ContainsTime will tell you if a golang time is contained // within the interval. type TimeInterval struct { - Times []timeRange `yaml:"times"` - Weekdays []weekdayRange `yaml:"weekdays"` - DaysOfMonth []dayOfMonthRange `yaml:"days_of_month"` - Months []monthRange `yaml:"months"` - Years []yearRange `yaml:"years"` + Times []TimeRange `yaml:"times,omitempty"` + Weekdays []WeekdayRange `yaml:"weekdays,flow,omitempty"` + DaysOfMonth []DayOfMonthRange `yaml:"days_of_month,flow,omitempty"` + Months []MonthRange `yaml:"months,flow,omitempty"` + Years []YearRange `yaml:"years,flow,omitempty"` } -/* TimeRange represents a range of minutes within a 1440 minute day, exclusive of the end minute. A day consists of 1440 minutes. - For example, 5:00PM to end of the day would begin at 1020 and end at 1440. */ -type timeRange struct { - startMinute int - endMinute int +/* TimeRange represents a range of minutes within a 1440 minute day, exclusive of the End minute. A day consists of 1440 minutes. + For example, 5:00PM to End of the day would Begin at 1020 and End at 1440. */ +type TimeRange struct { + StartMinute int + EndMinute int } -// inclusiveRange is used to hold the beginning and end values of many time interval components -type inclusiveRange struct { - begin int - end int +// InclusiveRange is used to hold the Beginning and End values of many time interval components +type InclusiveRange struct { + Begin int + End int } -// A weekdayRange is an inclusive range between [0, 6] where 0 = Sunday -type weekdayRange struct { - inclusiveRange +// A WeekdayRange is an inclusive range between [0, 6] where 0 = Sunday +type WeekdayRange struct { + InclusiveRange } -// A dayOfMonthRange is an inclusive range that may have negative beginning/end values that represent distance from the end of the month beginning at -1 -type dayOfMonthRange struct { - inclusiveRange +// A DayOfMonthRange is an inclusive range that may have negative Beginning/End values that represent distance from the End of the month Beginning at -1 +type DayOfMonthRange struct { + InclusiveRange } -// A monthRange is an inclusive range between [1, 12] where 1 = January -type monthRange struct { - inclusiveRange +// A MonthRange is an inclusive range between [1, 12] where 1 = January +type MonthRange struct { + InclusiveRange } -// A year range is a positive inclusive range -type yearRange struct { - inclusiveRange +// A YearRange is a positive inclusive range +type YearRange struct { + InclusiveRange } type yamlTimeRange struct { @@ -57,7 +57,7 @@ type yamlTimeRange struct { EndTime string `yaml:"end_time"` } -// A range with a beginning and end that can be represented as strings +// A range with a Beginning and End that can be represented as strings type stringableRange interface { setBegin(int) setEnd(int) @@ -65,15 +65,15 @@ type stringableRange interface { memberFromString(string) (int, error) } -func (ir *inclusiveRange) setBegin(n int) { - ir.begin = n +func (ir *InclusiveRange) setBegin(n int) { + ir.Begin = n } -func (ir *inclusiveRange) setEnd(n int) { - ir.end = n +func (ir *InclusiveRange) setEnd(n int) { + ir.End = n } -func (ir *inclusiveRange) memberFromString(in string) (out int, err error) { +func (ir *InclusiveRange) memberFromString(in string) (out int, err error) { out, err = strconv.Atoi(in) if err != nil { return -1, err @@ -81,7 +81,7 @@ func (ir *inclusiveRange) memberFromString(in string) (out int, err error) { return out, nil } -func (r *weekdayRange) memberFromString(in string) (out int, err error) { +func (r *WeekdayRange) memberFromString(in string) (out int, err error) { out, ok := daysOfWeek[in] if !ok { return -1, fmt.Errorf("%s is not a valid weekday", in) @@ -89,10 +89,13 @@ func (r *weekdayRange) memberFromString(in string) (out int, err error) { return out, nil } -func (r *monthRange) memberFromString(in string) (out int, err error) { +func (r *MonthRange) memberFromString(in string) (out int, err error) { out, ok := months[in] if !ok { - return -1, fmt.Errorf("%s is not a valid weekday", in) + out, err = strconv.Atoi(in) + if err != nil { + return -1, fmt.Errorf("%s is not a valid month", in) + } } return out, nil } @@ -106,6 +109,15 @@ var daysOfWeek = map[string]int{ "friday": 5, "saturday": 6, } +var daysOfWeekInv = map[int]string{ + 0: "sunday", + 1: "monday", + 2: "tuesday", + 3: "wednesday", + 4: "thursday", + 5: "friday", + 6: "saturday", +} var months = map[string]int{ "january": 1, @@ -122,111 +134,187 @@ var months = map[string]int{ "december": 12, } -func (r *weekdayRange) UnmarshalYAML(unmarshal func(interface{}) error) error { +var monthsInv = map[int]string{ + 1: "january", + 2: "february", + 3: "march", + 4: "april", + 5: "may", + 6: "june", + 7: "july", + 8: "august", + 9: "september", + 10: "october", + 11: "november", + 12: "december", +} + +// UnmarshalYAML implements the Unmarshaller interface for WeekdayRange. +func (r *WeekdayRange) UnmarshalYAML(unmarshal func(interface{}) error) error { var str string if err := unmarshal(&str); err != nil { return err } err := stringableRangeFromString(str, r) - if r.begin > r.end { - return errors.New("Start day cannot be before end day") + if r.Begin > r.End { + return errors.New("Start day cannot be before End day") } - if r.begin < 0 || r.begin > 6 { + if r.Begin < 0 || r.Begin > 6 { return fmt.Errorf("%s is not a valid day of the week: out of range", str) } - if r.end < 0 || r.end > 6 { + if r.End < 0 || r.End > 6 { return fmt.Errorf("%s is not a valid day of the week: out of range", str) } return err } -func (r *dayOfMonthRange) UnmarshalYAML(unmarshal func(interface{}) error) error { +// MarshalYAML implements the yaml.Marshaler interface for WeekdayRange +func (r WeekdayRange) MarshalYAML() (interface{}, error) { + beginStr, ok := daysOfWeekInv[r.Begin] + if !ok { + return nil, fmt.Errorf("Unable to convert %d into weekday string", r.Begin) + } + if r.Begin == r.End { + return interface{}(beginStr), nil + } + endStr, ok := daysOfWeekInv[r.End] + if !ok { + return nil, fmt.Errorf("Unable to convert %d into weekday string", r.End) + } + rangeStr := fmt.Sprintf("%s:%s", beginStr, endStr) + return interface{}(rangeStr), nil +} + +// UnmarshalYAML implements the Unmarshaller interface for DayOfMonthRange. +func (r *DayOfMonthRange) UnmarshalYAML(unmarshal func(interface{}) error) error { var str string if err := unmarshal(&str); err != nil { return err } err := stringableRangeFromString(str, r) - if r.begin == 0 || r.begin < -31 || r.begin > 31 { - return fmt.Errorf("%d is not a valid day of the month: out of range", r.begin) + if r.Begin == 0 || r.Begin < -31 || r.Begin > 31 { + return fmt.Errorf("%d is not a valid day of the month: out of range", r.Begin) } - if r.end == 0 || r.end < -31 || r.end > 31 { - return fmt.Errorf("%d is not a valid day of the month: out of range", r.end) + if r.End == 0 || r.End < -31 || r.End > 31 { + return fmt.Errorf("%d is not a valid day of the month: out of range", r.End) } - // Check beginning <= end accounting for negatives day of month indices - trueBegin := r.begin - trueEnd := r.end - if r.begin < 0 { - trueBegin = 30 + r.begin + // Check Beginning <= End accounting for negatives day of month indices + trueBegin := r.Begin + trueEnd := r.End + if r.Begin < 0 { + trueBegin = 30 + r.Begin } - if r.end < 0 { - trueEnd = 30 + r.end + if r.End < 0 { + trueEnd = 30 + r.End } if trueBegin > trueEnd { - return errors.New("Start day cannot be before end day") + return errors.New("Start day cannot be before End day") } return err } -func (r *monthRange) UnmarshalYAML(unmarshal func(interface{}) error) error { +// UnmarshalYAML implements the Unmarshaller interface for MonthRange. +func (r *MonthRange) UnmarshalYAML(unmarshal func(interface{}) error) error { var str string if err := unmarshal(&str); err != nil { return err } err := stringableRangeFromString(str, r) - if r.begin > r.end { - return errors.New("Start month cannot be before end month") + if r.Begin > r.End { + return errors.New("Start month cannot be before End month") } - if r.begin < 1 || r.begin > 12 { + if r.Begin < 1 || r.Begin > 12 { return fmt.Errorf("%s is not a valid month: out of range", str) } - if r.end < 1 || r.end > 12 { + if r.End < 1 || r.End > 12 { return fmt.Errorf("%s is not a valid month: out of range", str) } return err } -func (r *yearRange) UnmarshalYAML(unmarshal func(interface{}) error) error { +// MarshalYAML implements the yaml.Marshaler interface for DayOfMonthRange +func (r MonthRange) MarshalYAML() (interface{}, error) { + beginStr, ok := monthsInv[r.Begin] + if !ok { + return nil, fmt.Errorf("Unable to convert %d into month", r.Begin) + } + if r.Begin == r.End { + return interface{}(beginStr), nil + } + endStr, ok := monthsInv[r.End] + if !ok { + return nil, fmt.Errorf("Unable to convert %d into month", r.End) + } + rangeStr := fmt.Sprintf("%s:%s", beginStr, endStr) + return interface{}(rangeStr), nil +} + +// UnmarshalYAML implements the Unmarshaller interface for YearRange. +func (r *YearRange) UnmarshalYAML(unmarshal func(interface{}) error) error { var str string if err := unmarshal(&str); err != nil { return err } err := stringableRangeFromString(str, r) - if r.begin > r.end { - return errors.New("Start day cannot be before end day") + if r.Begin > r.End { + return errors.New("Start day cannot be before End day") } return err } -// UnmarshalYAML implements the Unmarshaller interface for timeRanges. -func (tr *timeRange) UnmarshalYAML(unmarshal func(interface{}) error) error { +// UnmarshalYAML implements the Unmarshaller interface for TimeRanges. +func (tr *TimeRange) UnmarshalYAML(unmarshal func(interface{}) error) error { var y yamlTimeRange if err := unmarshal(&y); err != nil { return err } if y.EndTime == "" || y.StartTime == "" { - return errors.New("Both start and end times must be provided") + return errors.New("Both start and End times must be provided") } start, err := parseTime(y.StartTime) if err != nil { return nil } - end, err := parseTime(y.EndTime) + End, err := parseTime(y.EndTime) if err != nil { return err } if start < 0 { return errors.New("Start time out of range") } - if end > 1440 { + if End > 1440 { return errors.New("End time out of range") } - if start >= end { - return errors.New("Start time cannot be equal or greater than end time") + if start >= End { + return errors.New("Start time cannot be equal or greater than End time") } - tr.startMinute, tr.endMinute = start, end + tr.StartMinute, tr.EndMinute = start, End return nil } +//MarshalYAML implements the yaml.Marshaler interface for TimeRange +func (tr TimeRange) MarshalYAML() (out interface{}, err error) { + startHr := tr.StartMinute / 60 + endHr := tr.EndMinute / 60 + startMin := tr.StartMinute % 60 + endMin := tr.EndMinute % 60 + + startStr := fmt.Sprintf("%02d:%02d", startHr, startMin) + endStr := fmt.Sprintf("%02d:%02d", endHr, endMin) + + yTr := yamlTimeRange{startStr, endStr} + return interface{}(yTr), err +} + +//MarshalYAML implements the yaml.Marshaler interface for InclusiveRange +func (ir InclusiveRange) MarshalYAML() (interface{}, error) { + if ir.Begin == ir.End { + return strconv.Itoa(ir.Begin), nil + } + out := fmt.Sprintf("%d:%d", ir.Begin, ir.End) + return interface{}(out), nil +} + // TimeLayout specifies the layout to be used in time.Parse() calls for time intervals const TimeLayout = "15:04" @@ -256,7 +344,7 @@ func (tp TimeInterval) ContainsTime(t time.Time) bool { if tp.Times != nil { in := false for _, validMinutes := range tp.Times { - if (t.Hour()*60+t.Minute()) >= validMinutes.startMinute && (t.Hour()*60+t.Minute()) < validMinutes.endMinute { + if (t.Hour()*60+t.Minute()) >= validMinutes.StartMinute && (t.Hour()*60+t.Minute()) < validMinutes.EndMinute { in = true break } @@ -268,22 +356,22 @@ func (tp TimeInterval) ContainsTime(t time.Time) bool { if tp.DaysOfMonth != nil { in := false for _, validDates := range tp.DaysOfMonth { - var begin, end int + var Begin, End int daysInMonth := daysInMonth(t) - if validDates.begin < 0 { - begin = daysInMonth + validDates.begin + 1 + if validDates.Begin < 0 { + Begin = daysInMonth + validDates.Begin + 1 } else { - begin = validDates.begin + Begin = validDates.Begin } - if validDates.end < 0 { - end = daysInMonth + validDates.end + 1 + if validDates.End < 0 { + End = daysInMonth + validDates.End + 1 } else { - end = validDates.end + End = validDates.End } // Clamp to the boundaries of the month to prevent crossing into other months - begin = clamp(begin, -1*daysInMonth, daysInMonth) - end = clamp(end, -1*daysInMonth, daysInMonth) - if t.Day() >= begin && t.Day() <= end { + Begin = clamp(Begin, -1*daysInMonth, daysInMonth) + End = clamp(End, -1*daysInMonth, daysInMonth) + if t.Day() >= Begin && t.Day() <= End { in = true break } @@ -295,7 +383,7 @@ func (tp TimeInterval) ContainsTime(t time.Time) bool { if tp.Months != nil { in := false for _, validMonths := range tp.Months { - if t.Month() >= time.Month(validMonths.begin) && t.Month() <= time.Month(validMonths.end) { + if t.Month() >= time.Month(validMonths.Begin) && t.Month() <= time.Month(validMonths.End) { in = true break } @@ -307,7 +395,7 @@ func (tp TimeInterval) ContainsTime(t time.Time) bool { if tp.Weekdays != nil { in := false for _, validDays := range tp.Weekdays { - if t.Weekday() >= time.Weekday(validDays.begin) && t.Weekday() <= time.Weekday(validDays.end) { + if t.Weekday() >= time.Weekday(validDays.Begin) && t.Weekday() <= time.Weekday(validDays.End) { in = true break } @@ -319,7 +407,7 @@ func (tp TimeInterval) ContainsTime(t time.Time) bool { if tp.Years != nil { in := false for _, validYears := range tp.Years { - if t.Year() >= validYears.begin && t.Year() <= validYears.end { + if t.Year() >= validYears.Begin && t.Year() <= validYears.End { in = true break } @@ -331,6 +419,7 @@ func (tp TimeInterval) ContainsTime(t time.Time) bool { return true } +// Converts a string of the form "HH:MM" into a TimeRange func parseTime(in string) (mins int, err error) { if !validTimeRE.MatchString(in) { return 0, fmt.Errorf("Couldn't parse timestamp %s, invalid format", in) @@ -355,6 +444,7 @@ func parseTime(in string) (mins int, err error) { return mins, nil } +// Converts a range that can be represented as strings (e.g. monday:wednesday) into an equivalent integer-represented range func stringableRangeFromString(in string, r stringableRange) (err error) { in = strings.ToLower(in) if strings.ContainsRune(in, ':') { @@ -366,12 +456,12 @@ func stringableRangeFromString(in string, r stringableRange) (err error) { if err != nil { return err } - end, err := r.memberFromString(components[1]) + End, err := r.memberFromString(components[1]) if err != nil { return err } r.setBegin(start) - r.setEnd(end) + r.setEnd(End) return nil } val, err := r.memberFromString(in) diff --git a/vendor/modules.txt b/vendor/modules.txt index 74156b5447..40a11ce986 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -12,7 +12,7 @@ github.com/alecthomas/units github.com/armon/go-metrics # github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496 github.com/asaskevich/govalidator -# github.com/benridley/gotime v0.0.1 +# github.com/benridley/gotime v0.0.2 ## explicit github.com/benridley/gotime # github.com/beorn7/perks v1.0.1 From d1f5e079093130ef557e119a137730fc986067f0 Mon Sep 17 00:00:00 2001 From: ben Date: Tue, 6 Oct 2020 21:07:42 +1100 Subject: [PATCH 08/47] Add mute time stage and pipeline Signed-off-by: Ben Ridley --- cmd/alertmanager/main.go | 8 ++++++ dispatch/dispatch.go | 1 + notify/notify.go | 59 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 67 insertions(+), 1 deletion(-) diff --git a/cmd/alertmanager/main.go b/cmd/alertmanager/main.go index e975de416e..8f568afe0d 100644 --- a/cmd/alertmanager/main.go +++ b/cmd/alertmanager/main.go @@ -28,6 +28,7 @@ import ( "syscall" "time" + "github.com/benridley/gotime" "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" "github.com/pkg/errors" @@ -413,6 +414,12 @@ func run() int { integrationsNum += len(integrations) } + // Build the map of time interval names to mute time definitions + muteTimes := make(map[string][]gotime.TimeInterval) + for _, ti := range conf.MuteTimeIntervals { + muteTimes[ti.Name] = ti.TimeIntervals + } + inhibitor.Stop() disp.Stop() @@ -423,6 +430,7 @@ func run() int { waitFunc, inhibitor, silencer, + muteTimes, notificationLog, peer, ) diff --git a/dispatch/dispatch.go b/dispatch/dispatch.go index d5233b5065..df55f9dfef 100644 --- a/dispatch/dispatch.go +++ b/dispatch/dispatch.go @@ -404,6 +404,7 @@ func (ag *aggrGroup) run(nf notifyFunc) { ctx = notify.WithGroupLabels(ctx, ag.labels) ctx = notify.WithReceiverName(ctx, ag.opts.Receiver) ctx = notify.WithRepeatInterval(ctx, ag.opts.RepeatInterval) + ctx = notify.WithMuteTimes(ctx, ag.opts.MuteTimes) // Wait the configured interval before calling flush again. ag.mtx.Lock() diff --git a/notify/notify.go b/notify/notify.go index 0be8b488be..d29bb3f2d0 100644 --- a/notify/notify.go +++ b/notify/notify.go @@ -20,6 +20,7 @@ import ( "sync" "time" + "github.com/benridley/gotime" "github.com/cenkalti/backoff/v4" "github.com/cespare/xxhash" "github.com/go-kit/kit/log" @@ -108,6 +109,7 @@ const ( keyFiringAlerts keyResolvedAlerts keyNow + keyMuteTimes ) // WithReceiverName populates a context with a receiver name. @@ -145,6 +147,11 @@ func WithRepeatInterval(ctx context.Context, t time.Duration) context.Context { return context.WithValue(ctx, keyRepeatInterval, t) } +// WithMuteTimes populates a context with a slice of mute time names. +func WithMuteTimes(ctx context.Context, mt []string) context.Context { + return context.WithValue(ctx, keyMuteTimes, mt) +} + // RepeatInterval extracts a repeat interval from the context. Iff none exists, the // second argument is false. func RepeatInterval(ctx context.Context) (time.Duration, bool) { @@ -194,6 +201,13 @@ func ResolvedAlerts(ctx context.Context) ([]uint64, bool) { return v, ok } +// MuteTimeNames extracts a slice of mute time names from the context. Iff none exists, the +// second argument is false. +func MuteTimeNames(ctx context.Context) ([]string, bool) { + v, ok := ctx.Value(keyMuteTimes).([]string) + return v, ok +} + // A Stage processes alerts under the constraints of the given context. type Stage interface { Exec(ctx context.Context, l log.Logger, alerts ...*types.Alert) (context.Context, []*types.Alert, error) @@ -289,6 +303,7 @@ func (pb *PipelineBuilder) New( wait func() time.Duration, inhibitor *inhibit.Inhibitor, silencer *silence.Silencer, + muteTimes map[string][]gotime.TimeInterval, notificationLog NotificationLog, peer *cluster.Peer, ) RoutingStage { @@ -297,10 +312,11 @@ func (pb *PipelineBuilder) New( ms := NewGossipSettleStage(peer) is := NewMuteStage(inhibitor) ss := NewMuteStage(silencer) + mts := NewTimeMuteStage(muteTimes) for name := range receivers { st := createReceiverStage(name, receivers[name], wait, notificationLog, pb.metrics) - rs[name] = MultiStage{ms, is, ss, st} + rs[name] = MultiStage{ms, is, mts, ss, st} } return rs } @@ -755,3 +771,44 @@ func (n SetNotifiesStage) Exec(ctx context.Context, l log.Logger, alerts ...*typ return ctx, alerts, n.nflog.Log(n.recv, gkey, firing, resolved) } + +type TimeMuteStage struct { + muteTimes map[string][]gotime.TimeInterval +} + +func NewTimeMuteStage(mt map[string][]gotime.TimeInterval) *TimeMuteStage { + return &TimeMuteStage{mt} +} + +// Exec implements the stage interface for TimeMuteStage +// TimeMuteStage is responsible for muting alerts whose route is not in an active time +func (mts TimeMuteStage) Exec(ctx context.Context, l log.Logger, alerts ...*types.Alert) (context.Context, []*types.Alert, error) { + muteTimeNames, ok := MuteTimeNames(ctx) + if !ok { + return ctx, alerts, nil + } + now, ok := Now(ctx) + if !ok { + return ctx, alerts, errors.New("missing now timestamp") + } + + muted := false + for _, mtName := range muteTimeNames { + mt, ok := mts.muteTimes[mtName] + if !ok { + return ctx, alerts, errors.Errorf("mute time %s doesn't exist in config", mtName) + } + for _, ti := range mt { + if ti.ContainsTime(now) { + muted = true + } + } + } + // If the current time is inside a mute time, all alerts are removed from the pipeline + if muted { + lvl := level.Warn(l) + lvl.Log("Mail not sent due to being outside time interval") + return ctx, nil, nil + } + return ctx, alerts, nil +} From f53e7a984c93c3cf6bfe184c101772e964c24d86 Mon Sep 17 00:00:00 2001 From: Ben Ridley Date: Tue, 13 Oct 2020 10:52:48 +1100 Subject: [PATCH 09/47] Add tests for TimeMuteStage Signed-off-by: Ben Ridley --- notify/notify_test.go | 75 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/notify/notify_test.go b/notify/notify_test.go index d286ef0a78..87c1054ca2 100644 --- a/notify/notify_test.go +++ b/notify/notify_test.go @@ -22,10 +22,12 @@ import ( "testing" "time" + "github.com/benridley/gotime" "github.com/go-kit/kit/log" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/common/model" "github.com/stretchr/testify/require" + "gopkg.in/yaml.v2" "github.com/prometheus/alertmanager/nflog" "github.com/prometheus/alertmanager/nflog/nflogpb" @@ -719,3 +721,76 @@ func TestMuteStageWithSilences(t *testing.T) { t.Fatalf("Unmuting failed, expected: %v\ngot %v", in, got) } } + +func TestTimeMuteStage(t *testing.T) { + cases := []struct { + fireTime string + labels model.LabelSet + shouldMute bool + }{ + { + fireTime: "01 Jan 21 09:00 GMT", + labels: model.LabelSet{"dont": "mute"}, + shouldMute: false, + }, + { + fireTime: "01 Dec 20 16:59 GMT", + labels: model.LabelSet{"dont": "mute"}, + shouldMute: false, + }, + { + fireTime: "17 Oct 20 10:00 GMT", + labels: model.LabelSet{"mute": "me"}, + shouldMute: true, + }, + } + // Route mutes alerts outside business hours + muteIn := ` +--- +- weekdays: ['monday:friday'] + times: + - start_time: '00:00' + end_time: '09:00' + - start_time: '17:00' + end_time: '24:00' +- weekdays: ['saturday', 'sunday']` + var intervals []gotime.TimeInterval + err := yaml.Unmarshal([]byte(muteIn), &intervals) + if err != nil { + t.Fatalf("Couldn't unmarshal time interval %s", err) + } + m := map[string][]gotime.TimeInterval{"test": intervals} + stage := NewTimeMuteStage(m) + + outAlerts := []*types.Alert{} + nonMuteCount := 0 + for _, tc := range cases { + now, err := time.Parse(time.RFC822, tc.fireTime) + if err != nil { + t.Fatalf("Couldn't parse fire time %s %s", tc.fireTime, err) + } + // Count alerts with shouldMute == false and compare to ensure none are muted incorrectly + if !tc.shouldMute { + nonMuteCount++ + } + a := model.Alert{Labels: tc.labels} + alerts := []*types.Alert{{Alert: a}} + ctx := context.Background() + ctx = WithNow(ctx, now) + ctx = WithMuteTimes(ctx, []string{"test"}) + + _, out, err := stage.Exec(ctx, log.NewNopLogger(), alerts...) + if err != nil { + t.Fatalf("Unexpected error in time mute stage %s", err) + } + outAlerts = append(outAlerts, out...) + } + for _, alert := range outAlerts { + if _, ok := alert.Alert.Labels["mute"]; ok { + t.Fatalf("Expected alert to be muted %+v", alert.Alert) + } + } + if len(outAlerts) != nonMuteCount { + t.Fatalf("Expected %d alerts after time mute stage but got %d", nonMuteCount, len(outAlerts)) + } +} From a3cb125e5c62a5db25d3cf9f292d946044154470 Mon Sep 17 00:00:00 2001 From: Ben Ridley Date: Tue, 13 Oct 2020 11:04:16 +1100 Subject: [PATCH 10/47] Move timeinterval library into locally maintained package Signed-off-by: Ben Ridley --- cmd/alertmanager/main.go | 4 +- config/config.go | 6 +- go.mod | 1 - go.sum | 4 - notes.txt | 2 - notify/notify.go | 8 +- notify/notify_test.go | 38 +- .../gotime.go => timeinterval/timeinterval.go | 2 +- timeinterval/timeinterval_test.go | 430 ++++++++++++++++++ vendor/github.com/benridley/gotime/.gitignore | 15 - vendor/github.com/benridley/gotime/README.md | 15 - vendor/github.com/benridley/gotime/go.mod | 5 - vendor/github.com/benridley/gotime/go.sum | 4 - vendor/modules.txt | 3 - 14 files changed, 464 insertions(+), 73 deletions(-) delete mode 100644 notes.txt rename vendor/github.com/benridley/gotime/gotime.go => timeinterval/timeinterval.go (99%) create mode 100644 timeinterval/timeinterval_test.go delete mode 100644 vendor/github.com/benridley/gotime/.gitignore delete mode 100644 vendor/github.com/benridley/gotime/README.md delete mode 100644 vendor/github.com/benridley/gotime/go.mod delete mode 100644 vendor/github.com/benridley/gotime/go.sum diff --git a/cmd/alertmanager/main.go b/cmd/alertmanager/main.go index 8f568afe0d..7e8aaba532 100644 --- a/cmd/alertmanager/main.go +++ b/cmd/alertmanager/main.go @@ -28,7 +28,6 @@ import ( "syscall" "time" - "github.com/benridley/gotime" "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" "github.com/pkg/errors" @@ -61,6 +60,7 @@ import ( "github.com/prometheus/alertmanager/provider/mem" "github.com/prometheus/alertmanager/silence" "github.com/prometheus/alertmanager/template" + "github.com/prometheus/alertmanager/timeinterval" "github.com/prometheus/alertmanager/types" "github.com/prometheus/alertmanager/ui" ) @@ -415,7 +415,7 @@ func run() int { } // Build the map of time interval names to mute time definitions - muteTimes := make(map[string][]gotime.TimeInterval) + muteTimes := make(map[string][]timeinterval.TimeInterval) for _, ti := range conf.MuteTimeIntervals { muteTimes[ti.Name] = ti.TimeIntervals } diff --git a/config/config.go b/config/config.go index e17025e9ed..5d127f720e 100644 --- a/config/config.go +++ b/config/config.go @@ -25,8 +25,8 @@ import ( "strings" "time" - "github.com/benridley/gotime" "github.com/pkg/errors" + "github.com/prometheus/alertmanager/timeinterval" commoncfg "github.com/prometheus/common/config" "github.com/prometheus/common/model" "gopkg.in/yaml.v2" @@ -222,8 +222,8 @@ func resolveFilepaths(baseDir string, cfg *Config) { // A MuteTimeInterval represents a named set of time intervals for which a route should be muted. type MuteTimeInterval struct { - Name string `yaml:"name" json:"name"` - TimeIntervals []gotime.TimeInterval `yaml:"time_intervals"` + Name string `yaml:"name" json:"name"` + TimeIntervals []timeinterval.TimeInterval `yaml:"time_intervals"` } // Config is the top-level configuration for Alertmanager's config files. diff --git a/go.mod b/go.mod index 7123dba512..09c54bbaf7 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,6 @@ module github.com/prometheus/alertmanager require ( github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d - github.com/benridley/gotime v0.0.2 github.com/cenkalti/backoff/v4 v4.0.2 github.com/cespare/xxhash v1.1.0 github.com/go-kit/kit v0.10.0 diff --git a/go.sum b/go.sum index 48286e5537..422121b57e 100644 --- a/go.sum +++ b/go.sum @@ -38,10 +38,6 @@ github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:o github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= -github.com/benridley/gotime v0.0.1 h1:wd8RPtHpAev+8FyOT7SoxV5nfTrpu6vBBMooAAdiNLg= -github.com/benridley/gotime v0.0.1/go.mod h1:/leEq6n8vU9CTErhe8vZI5sZPnRMMTP1/YaDzoagvwg= -github.com/benridley/gotime v0.0.2 h1:n9OEcwxr57tXcqhoWTm3uDClVzttDNcCydiMNXyGqq0= -github.com/benridley/gotime v0.0.2/go.mod h1:/leEq6n8vU9CTErhe8vZI5sZPnRMMTP1/YaDzoagvwg= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= diff --git a/notes.txt b/notes.txt deleted file mode 100644 index ad57096a01..0000000000 --- a/notes.txt +++ /dev/null @@ -1,2 +0,0 @@ -- Unmarshalling is weird. Alertmanager has a status page that just marshalls config into a string. When I tried to use my library with it, it blew up because the timeinterval library hides many of its internal fields and thus they can't be marshalled. - I worked around this by creating a 'config.TimeInterval' concept. Real timeintervals can be created from this representation by re-unmarshalling maybe? diff --git a/notify/notify.go b/notify/notify.go index d29bb3f2d0..dd4c13de15 100644 --- a/notify/notify.go +++ b/notify/notify.go @@ -20,7 +20,6 @@ import ( "sync" "time" - "github.com/benridley/gotime" "github.com/cenkalti/backoff/v4" "github.com/cespare/xxhash" "github.com/go-kit/kit/log" @@ -34,6 +33,7 @@ import ( "github.com/prometheus/alertmanager/nflog" "github.com/prometheus/alertmanager/nflog/nflogpb" "github.com/prometheus/alertmanager/silence" + "github.com/prometheus/alertmanager/timeinterval" "github.com/prometheus/alertmanager/types" ) @@ -303,7 +303,7 @@ func (pb *PipelineBuilder) New( wait func() time.Duration, inhibitor *inhibit.Inhibitor, silencer *silence.Silencer, - muteTimes map[string][]gotime.TimeInterval, + muteTimes map[string][]timeinterval.TimeInterval, notificationLog NotificationLog, peer *cluster.Peer, ) RoutingStage { @@ -773,10 +773,10 @@ func (n SetNotifiesStage) Exec(ctx context.Context, l log.Logger, alerts ...*typ } type TimeMuteStage struct { - muteTimes map[string][]gotime.TimeInterval + muteTimes map[string][]timeinterval.TimeInterval } -func NewTimeMuteStage(mt map[string][]gotime.TimeInterval) *TimeMuteStage { +func NewTimeMuteStage(mt map[string][]timeinterval.TimeInterval) *TimeMuteStage { return &TimeMuteStage{mt} } diff --git a/notify/notify_test.go b/notify/notify_test.go index 87c1054ca2..cc6cfd2e7a 100644 --- a/notify/notify_test.go +++ b/notify/notify_test.go @@ -22,7 +22,6 @@ import ( "testing" "time" - "github.com/benridley/gotime" "github.com/go-kit/kit/log" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/common/model" @@ -33,6 +32,7 @@ import ( "github.com/prometheus/alertmanager/nflog/nflogpb" "github.com/prometheus/alertmanager/silence" "github.com/prometheus/alertmanager/silence/silencepb" + "github.com/prometheus/alertmanager/timeinterval" "github.com/prometheus/alertmanager/types" ) @@ -723,43 +723,53 @@ func TestMuteStageWithSilences(t *testing.T) { } func TestTimeMuteStage(t *testing.T) { + // Route mutes alerts outside business hours + muteIn := ` +--- +- weekdays: ['monday:friday'] + times: + - start_time: '00:00' + end_time: '09:00' + - start_time: '17:00' + end_time: '24:00' +- weekdays: ['saturday', 'sunday']` + cases := []struct { fireTime string labels model.LabelSet shouldMute bool }{ { + // Friday during business hours fireTime: "01 Jan 21 09:00 GMT", - labels: model.LabelSet{"dont": "mute"}, + labels: model.LabelSet{"foo": "bar"}, shouldMute: false, }, { + // Tuesday before 5pm fireTime: "01 Dec 20 16:59 GMT", labels: model.LabelSet{"dont": "mute"}, shouldMute: false, }, { + // Saturday fireTime: "17 Oct 20 10:00 GMT", labels: model.LabelSet{"mute": "me"}, shouldMute: true, }, + { + // Wednesday before 9am + fireTime: "14 Oct 20 05:00 GMT", + labels: model.LabelSet{"mute": "me"}, + shouldMute: true, + }, } - // Route mutes alerts outside business hours - muteIn := ` ---- -- weekdays: ['monday:friday'] - times: - - start_time: '00:00' - end_time: '09:00' - - start_time: '17:00' - end_time: '24:00' -- weekdays: ['saturday', 'sunday']` - var intervals []gotime.TimeInterval + var intervals []timeinterval.TimeInterval err := yaml.Unmarshal([]byte(muteIn), &intervals) if err != nil { t.Fatalf("Couldn't unmarshal time interval %s", err) } - m := map[string][]gotime.TimeInterval{"test": intervals} + m := map[string][]timeinterval.TimeInterval{"test": intervals} stage := NewTimeMuteStage(m) outAlerts := []*types.Alert{} diff --git a/vendor/github.com/benridley/gotime/gotime.go b/timeinterval/timeinterval.go similarity index 99% rename from vendor/github.com/benridley/gotime/gotime.go rename to timeinterval/timeinterval.go index de2f399f18..f98c11ba33 100644 --- a/vendor/github.com/benridley/gotime/gotime.go +++ b/timeinterval/timeinterval.go @@ -1,4 +1,4 @@ -package gotime +package timeinterval import ( "errors" diff --git a/timeinterval/timeinterval_test.go b/timeinterval/timeinterval_test.go new file mode 100644 index 0000000000..74d2d233bd --- /dev/null +++ b/timeinterval/timeinterval_test.go @@ -0,0 +1,430 @@ +package timeinterval + +import ( + "reflect" + "testing" + "time" + + "gopkg.in/yaml.v2" +) + +var timeIntervalTestCases = []struct { + validTimeStrings []string + invalidTimeStrings []string + timeInterval TimeInterval +}{ + { + timeInterval: TimeInterval{}, + validTimeStrings: []string{ + "02 Jan 06 15:04 MST", + "03 Jan 07 10:04 MST", + "04 Jan 06 09:04 MST", + }, + invalidTimeStrings: []string{}, + }, + { + // 9am to 5pm, monday to friday + timeInterval: TimeInterval{ + Times: []TimeRange{{StartMinute: 540, EndMinute: 1020}}, + Weekdays: []WeekdayRange{{InclusiveRange{Begin: 1, End: 5}}}, + }, + validTimeStrings: []string{ + "04 May 20 15:04 MST", + "05 May 20 10:04 MST", + "09 Jun 20 09:04 MST", + }, + invalidTimeStrings: []string{ + "03 May 20 15:04 MST", + "04 May 20 08:59 MST", + "05 May 20 05:00 MST", + }, + }, + { + // Easter 2020 + timeInterval: TimeInterval{ + DaysOfMonth: []DayOfMonthRange{{InclusiveRange{Begin: 4, End: 6}}}, + Months: []MonthRange{{InclusiveRange{Begin: 4, End: 4}}}, + Years: []YearRange{{InclusiveRange{Begin: 2020, End: 2020}}}, + }, + validTimeStrings: []string{ + "04 Apr 20 15:04 MST", + "05 Apr 20 00:00 MST", + "06 Apr 20 23:05 MST", + }, + invalidTimeStrings: []string{ + "03 May 18 15:04 MST", + "03 Apr 20 23:59 MST", + "04 Jun 20 23:59 MST", + "06 Apr 19 23:59 MST", + "07 Apr 20 00:00 MST", + }, + }, + { + // Check negative days of month, last 3 days of each month + timeInterval: TimeInterval{ + DaysOfMonth: []DayOfMonthRange{{InclusiveRange{Begin: -3, End: -1}}}, + }, + validTimeStrings: []string{ + "31 Jan 20 15:04 MST", + "30 Jan 20 15:04 MST", + "29 Jan 20 15:04 MST", + "30 Jun 20 00:00 MST", + "29 Feb 20 23:05 MST", + }, + invalidTimeStrings: []string{ + "03 May 18 15:04 MST", + "27 Jan 20 15:04 MST", + "03 Apr 20 23:59 MST", + "04 Jun 20 23:59 MST", + "06 Apr 19 23:59 MST", + "07 Apr 20 00:00 MST", + "01 Mar 20 00:00 MST", + }, + }, + { + // Check out of bound days are clamped to month boundaries + timeInterval: TimeInterval{ + Months: []MonthRange{{InclusiveRange{Begin: 6, End: 6}}}, + DaysOfMonth: []DayOfMonthRange{{InclusiveRange{Begin: -31, End: 31}}}, + }, + validTimeStrings: []string{ + "30 Jun 20 00:00 MST", + "01 Jun 20 00:00 MST", + }, + invalidTimeStrings: []string{ + "31 May 20 00:00 MST", + "1 Jul 20 00:00 MST", + }, + }, +} + +var timeStringTestCases = []struct { + timeString string + TimeRange TimeRange + expectError bool +}{ + { + timeString: "{'start_time': '00:00', 'end_time': '24:00'}", + TimeRange: TimeRange{StartMinute: 0, EndMinute: 1440}, + expectError: false, + }, + { + timeString: "{'start_time': '01:35', 'end_time': '17:39'}", + TimeRange: TimeRange{StartMinute: 95, EndMinute: 1059}, + expectError: false, + }, + { + timeString: "{'start_time': '09:35', 'end_time': '09:39'}", + TimeRange: TimeRange{StartMinute: 575, EndMinute: 579}, + expectError: false, + }, + { + // Error: Begin and End times are the same + timeString: "{'start_time': '17:31', 'end_time': '17:31'}", + TimeRange: TimeRange{}, + expectError: true, + }, + { + // Error: End time out of range + timeString: "{'start_time': '12:30', 'end_time': '24:01'}", + TimeRange: TimeRange{}, + expectError: true, + }, + { + // Error: Start time greater than End time + timeString: "{'start_time': '09:30', 'end_time': '07:41'}", + TimeRange: TimeRange{}, + expectError: true, + }, + { + // Error: Start time out of range and greater than End time + timeString: "{'start_time': '24:00', 'end_time': '17:41'}", + TimeRange: TimeRange{}, + expectError: true, + }, + { + // Error: No range specified + timeString: "{'start_time': '14:03'}", + TimeRange: TimeRange{}, + expectError: true, + }, +} + +var dayOfWeekStringTestCases = []struct { + dowString string + ranges []WeekdayRange + expectError bool +}{ + { + dowString: "['monday:friday', 'saturday']", + ranges: []WeekdayRange{{InclusiveRange{Begin: 1, End: 5}}, {InclusiveRange{Begin: 6, End: 6}}}, + expectError: false, + }, +} + +var yamlUnmarshalTestCases = []struct { + in string + intervals []TimeInterval + contains []string + excludes []string + expectError bool +}{ + { + // Simple business hours test + in: ` +--- +- weekdays: ['monday:friday'] + times: + - start_time: '09:00' + end_time: '17:00' +`, + intervals: []TimeInterval{ + { + Weekdays: []WeekdayRange{{InclusiveRange{Begin: 1, End: 5}}}, + Times: []TimeRange{{StartMinute: 540, EndMinute: 1020}}, + }, + }, + contains: []string{ + "08 Jul 20 09:00 MST", + "08 Jul 20 16:59 MST", + }, + excludes: []string{ + "08 Jul 20 05:00 MST", + "08 Jul 20 08:59 MST", + }, + expectError: false, + }, + { + // More advanced test with negative indices and ranges + in: ` +--- + # Last week, excluding Saturday, of the first quarter of the year during business hours from 2020 to 2025 and 2030-2035 +- weekdays: ['monday:friday', 'sunday'] + months: ['january:march'] + days_of_month: ['-7:-1'] + years: ['2020:2025', '2030:2035'] + times: + - start_time: '09:00' + end_time: '17:00' +`, + intervals: []TimeInterval{ + { + Weekdays: []WeekdayRange{{InclusiveRange{Begin: 1, End: 5}}, {InclusiveRange{Begin: 0, End: 0}}}, + Times: []TimeRange{{StartMinute: 540, EndMinute: 1020}}, + Months: []MonthRange{{InclusiveRange{1, 3}}}, + DaysOfMonth: []DayOfMonthRange{{InclusiveRange{-7, -1}}}, + Years: []YearRange{{InclusiveRange{2020, 2025}}, {InclusiveRange{2030, 2035}}}, + }, + }, + contains: []string{ + "27 Jan 21 09:00 MST", + "28 Jan 21 16:59 MST", + "29 Jan 21 13:00 MST", + "31 Mar 25 13:00 MST", + "31 Mar 25 13:00 MST", + "31 Jan 35 13:00 MST", + }, + excludes: []string{ + "30 Jan 21 13:00 MST", // Saturday + "01 Apr 21 13:00 MST", // 4th month + "30 Jan 26 13:00 MST", // 2026 + "31 Jan 35 17:01 MST", // After 5pm + }, + expectError: false, + }, + { + in: ` +--- +- weekdays: ['monday:friday'] + times: + - start_time: '09:00' + end_time: '17:00'`, + intervals: []TimeInterval{ + { + Weekdays: []WeekdayRange{{InclusiveRange{Begin: 1, End: 5}}}, + Times: []TimeRange{{StartMinute: 540, EndMinute: 1020}}, + }, + }, + contains: []string{ + "01 Apr 21 13:00 GMT", + }, + }, + { + // Start day before End day + in: ` +--- +- weekdays: ['friday:monday']`, + expectError: true, + }, + { + // Invalid weekdays + in: ` +--- +- weekdays: ['blurgsday:flurgsday'] +`, + expectError: true, + }, + { + // 0 day of month + in: ` +--- +- days_of_month: ['0'] +`, + expectError: true, + }, + { + // Too early day of month + in: ` +--- +- days_of_month: ['-50:-20'] +`, + expectError: true, + }, + { + // Negative indices should work + in: ` +--- +- days_of_month: ['1:-1'] +`, + intervals: []TimeInterval{ + { + DaysOfMonth: []DayOfMonthRange{{InclusiveRange{1, -1}}}, + }, + }, + expectError: false, + }, + { + // Negative start date before positive End date + in: ` +--- +- days_of_month: ['-15:5'] +`, + expectError: true, + }, + { + // Negative End date before positive postive start date + in: ` +--- +- days_of_month: ['10:-25'] +`, + expectError: true, + }, +} + +func TestYamlUnmarshal(t *testing.T) { + for _, tc := range yamlUnmarshalTestCases { + var ti []TimeInterval + err := yaml.Unmarshal([]byte(tc.in), &ti) + if err != nil && !tc.expectError { + t.Errorf("Received unexpected error: %v when parsing %v", err, tc.in) + } else if err == nil && tc.expectError { + t.Errorf("Expected error when unmarshalling %s but didn't receive one", tc.in) + } else if err != nil && tc.expectError { + continue + } + if !reflect.DeepEqual(ti, tc.intervals) { + t.Errorf("Error unmarshalling %s: Want %+v, got %+v", tc.in, tc.intervals, ti) + } + for _, ts := range tc.contains { + _t, _ := time.Parse(time.RFC822, ts) + isContained := false + for _, interval := range ti { + if interval.ContainsTime(_t) { + isContained = true + } + } + if !isContained { + t.Errorf("Expected intervals to contain time %s", _t) + } + } + for _, ts := range tc.excludes { + _t, _ := time.Parse(time.RFC822, ts) + isContained := false + for _, interval := range ti { + if interval.ContainsTime(_t) { + isContained = true + } + } + if isContained { + t.Errorf("Expected intervals to exclude time %s", _t) + } + } + } +} + +func TestContainsTime(t *testing.T) { + for _, tc := range timeIntervalTestCases { + for _, ts := range tc.validTimeStrings { + _t, _ := time.Parse(time.RFC822, ts) + if !tc.timeInterval.ContainsTime(_t) { + t.Errorf("Expected period %+v to contain %+v", tc.timeInterval, _t) + } + } + for _, ts := range tc.invalidTimeStrings { + _t, _ := time.Parse(time.RFC822, ts) + if tc.timeInterval.ContainsTime(_t) { + t.Errorf("Period %+v not expected to contain %+v", tc.timeInterval, _t) + } + } + } +} + +func TestParseTimeString(t *testing.T) { + for _, tc := range timeStringTestCases { + var tr TimeRange + err := yaml.Unmarshal([]byte(tc.timeString), &tr) + if err != nil && !tc.expectError { + t.Errorf("Received unexpected error: %v when parsing %v", err, tc.timeString) + } else if err == nil && tc.expectError { + t.Errorf("Expected error for invalid string %s but didn't receive one", tc.timeString) + } else if !reflect.DeepEqual(tr, tc.TimeRange) { + t.Errorf("Error parsing time string %s: Want %+v, got %+v", tc.timeString, tc.TimeRange, tr) + } + } +} + +func TestParseWeek(t *testing.T) { + for _, tc := range dayOfWeekStringTestCases { + var wr []WeekdayRange + err := yaml.Unmarshal([]byte(tc.dowString), &wr) + if err != nil && !tc.expectError { + t.Errorf("Received unexpected error: %v when parsing %v", err, tc.dowString) + } else if err == nil && tc.expectError { + t.Errorf("Expected error for invalid string %s but didn't receive one", tc.dowString) + } else if !reflect.DeepEqual(wr, tc.ranges) { + t.Errorf("Error parsing time string %s: Want %+v, got %+v", tc.dowString, tc.ranges, wr) + } + } +} + +func TestYamlMarshal(t *testing.T) { + for _, tc := range yamlUnmarshalTestCases { + if tc.expectError { + continue + } + var ti []TimeInterval + err := yaml.Unmarshal([]byte(tc.in), &ti) + if err != nil { + t.Error(err) + } + out, err := yaml.Marshal(&ti) + if err != nil { + t.Error(err) + } + var ti2 []TimeInterval + yaml.Unmarshal(out, &ti2) + if !reflect.DeepEqual(ti, ti2) { + t.Errorf("Re-marshalling %s produced a different TimeInterval", tc.in) + } + } +} + +func emptyInterval() TimeInterval { + return TimeInterval{ + Times: []TimeRange{}, + Weekdays: []WeekdayRange{}, + DaysOfMonth: []DayOfMonthRange{}, + Months: []MonthRange{}, + Years: []YearRange{}, + } +} diff --git a/vendor/github.com/benridley/gotime/.gitignore b/vendor/github.com/benridley/gotime/.gitignore deleted file mode 100644 index 66fd13c903..0000000000 --- a/vendor/github.com/benridley/gotime/.gitignore +++ /dev/null @@ -1,15 +0,0 @@ -# Binaries for programs and plugins -*.exe -*.exe~ -*.dll -*.so -*.dylib - -# Test binary, built with `go test -c` -*.test - -# Output of the go coverage tool, specifically when used with LiteIDE -*.out - -# Dependency directories (remove the comment below to include it) -# vendor/ diff --git a/vendor/github.com/benridley/gotime/README.md b/vendor/github.com/benridley/gotime/README.md deleted file mode 100644 index 15f14487bb..0000000000 --- a/vendor/github.com/benridley/gotime/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# gotime -A go library for defining windows of time and validating points in time against those periods. - -# How to Use -The main struct, the TimeInterval, is designed to be instantiated by a yaml configuration file: -```yaml - # Last week, excluding Saturday, of the first quarter of the year during business hours from 2020 to 2025 and 2030-2035 -- weekdays: ['monday:friday', 'sunday'] - months: ['january:march'] - days_of_month: ['-7:-1'] - years: ['2020:2025', '2030:2035'] - times: - - start_time: '09:00' - end_time: '17:00' -``` diff --git a/vendor/github.com/benridley/gotime/go.mod b/vendor/github.com/benridley/gotime/go.mod deleted file mode 100644 index 9e7b2ccb2a..0000000000 --- a/vendor/github.com/benridley/gotime/go.mod +++ /dev/null @@ -1,5 +0,0 @@ -module github.com/benridley/gotime - -go 1.14 - -require gopkg.in/yaml.v2 v2.3.0 diff --git a/vendor/github.com/benridley/gotime/go.sum b/vendor/github.com/benridley/gotime/go.sum deleted file mode 100644 index 168980da5f..0000000000 --- a/vendor/github.com/benridley/gotime/go.sum +++ /dev/null @@ -1,4 +0,0 @@ -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/vendor/modules.txt b/vendor/modules.txt index 40a11ce986..4c06197203 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -12,9 +12,6 @@ github.com/alecthomas/units github.com/armon/go-metrics # github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496 github.com/asaskevich/govalidator -# github.com/benridley/gotime v0.0.2 -## explicit -github.com/benridley/gotime # github.com/beorn7/perks v1.0.1 github.com/beorn7/perks/quantile # github.com/cenkalti/backoff/v4 v4.0.2 From 93e0117b46b0e060d924a9f099b5987999603b13 Mon Sep 17 00:00:00 2001 From: Ben Ridley Date: Tue, 13 Oct 2020 11:06:31 +1100 Subject: [PATCH 11/47] Change logging to debug when notifications aren't sent due to route mute Signed-off-by: Ben Ridley --- notify/notify.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/notify/notify.go b/notify/notify.go index dd4c13de15..dd776819a9 100644 --- a/notify/notify.go +++ b/notify/notify.go @@ -806,8 +806,8 @@ func (mts TimeMuteStage) Exec(ctx context.Context, l log.Logger, alerts ...*type } // If the current time is inside a mute time, all alerts are removed from the pipeline if muted { - lvl := level.Warn(l) - lvl.Log("Mail not sent due to being outside time interval") + lvl := level.Debug(l) + lvl.Log("msg", "Notifications not sent, route is within mute time") return ctx, nil, nil } return ctx, alerts, nil From c51e598f914d8f5dae4f9027bffa6f42aa207650 Mon Sep 17 00:00:00 2001 From: Ben Ridley Date: Tue, 13 Oct 2020 11:24:21 +1100 Subject: [PATCH 12/47] Remove container testing script Signed-off-by: Ben Ridley --- make_container.sh | 7 ------- 1 file changed, 7 deletions(-) delete mode 100755 make_container.sh diff --git a/make_container.sh b/make_container.sh deleted file mode 100755 index 8155d6e92b..0000000000 --- a/make_container.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/sh -container=$(buildah from prom/alertmanager) -mnt=$(buildah mount $container) -CGO_ENABLED=0 go build -ldflags="-X 'main.Version=benny'" -o alertmanager cmd/alertmanager/main.go -cp -f ./alertmanager "${mnt}/bin/alertmanager" -buildah commit $container ben-am -buildah unmount $container \ No newline at end of file From 11c24d4ae63eb74e0e1900012d282fd9f117fe10 Mon Sep 17 00:00:00 2001 From: Ben Ridley Date: Tue, 13 Oct 2020 11:28:10 +1100 Subject: [PATCH 13/47] Correct case from renaming Signed-off-by: Ben Ridley --- timeinterval/timeinterval.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/timeinterval/timeinterval.go b/timeinterval/timeinterval.go index f98c11ba33..54cfa6d17a 100644 --- a/timeinterval/timeinterval.go +++ b/timeinterval/timeinterval.go @@ -356,22 +356,22 @@ func (tp TimeInterval) ContainsTime(t time.Time) bool { if tp.DaysOfMonth != nil { in := false for _, validDates := range tp.DaysOfMonth { - var Begin, End int + var begin, end int daysInMonth := daysInMonth(t) if validDates.Begin < 0 { - Begin = daysInMonth + validDates.Begin + 1 + begin = daysInMonth + validDates.Begin + 1 } else { - Begin = validDates.Begin + begin = validDates.Begin } if validDates.End < 0 { - End = daysInMonth + validDates.End + 1 + end = daysInMonth + validDates.End + 1 } else { - End = validDates.End + end = validDates.End } // Clamp to the boundaries of the month to prevent crossing into other months - Begin = clamp(Begin, -1*daysInMonth, daysInMonth) - End = clamp(End, -1*daysInMonth, daysInMonth) - if t.Day() >= Begin && t.Day() <= End { + begin = clamp(begin, -1*daysInMonth, daysInMonth) + end = clamp(end, -1*daysInMonth, daysInMonth) + if t.Day() >= begin && t.Day() <= end { in = true break } From 3d97ee55eba8148c3ddd21a1e65202bc8855523d Mon Sep 17 00:00:00 2001 From: Ben Ridley Date: Tue, 13 Oct 2020 12:21:53 +1100 Subject: [PATCH 14/47] Update docs to include mute time sections Signed-off-by: Ben Ridley --- docs/configuration.md | 66 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/docs/configuration.md b/docs/configuration.md index 303de67c49..d6e41102c8 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -113,6 +113,10 @@ receivers: # A list of inhibition rules. inhibit_rules: [ - ... ] + +# A list of mute time intervals for muting routes +mute_time_intervals: + [ - ... ] ``` ## `` @@ -168,6 +172,12 @@ match_re: # been sent successfully for an alert. (Usually ~3h or more). [ repeat_interval: | default = 4h ] +# Times when the route should be muted. These must match a name of a +# mute time interval defined in the mute_time_intervals section. +# Additionally, the root node cannot have any mute times. +mute_times: + [ - ...] + # Zero or more child routes. routes: [ - ... ] @@ -202,6 +212,62 @@ route: team: frontend ``` +## `` + +A `mute_time_interval` specifies a named interval of time that may be referenced +in the routing tree to mute particular routes for particular times of the day. + +```yaml +name: +time_intervals: + [ - ... ] + +``` +## `` +A `time_interval` contains the actual definition for an interval of time. The syntax +supports the following fields: + +```yaml +- times: + [ - ...] + weekdays: + [ - ...] + days_of_month: + [ - ...] + months: + [ - ...] + years: + [ - ...] +``` + +All these fields are optional and if left unspecified allow any value to match the interval. +Some fields support ranges and negative indices, and are detailed below: + +`times`: A list of time-ranges. They are inclusive of the starting time and exclusive +of the ending time to make it easy to represent times that start/end on hour boundaries. +For example, start_time: '17:00' and end_time: '24:00' will begin at 17:00 and finish +immediately after 23:59. They are specified like so: + + times: + - start_time: HH:MM + end_time: HH:MM + +`weekdays`: A list of days of the week, where the week begins on Sunday and ends on Saturday. +Days should be specified by name (e.g. ‘Sunday’). For convenience, ranges are also accepted +of the form : and are inclusive on both ends. For example: +`[‘monday:wednesday','saturday', 'sunday']` + +`days_of_month`: A list of numerical days in the month. Days begin at 1. +Negative values are also accepted which begin at the end of the month, +e.g. -1 during January would represent January 31. For example: `['1:5', '-3:-1']`. +Extending past the start or end of the month will cause it to be clamped. E.g. specifying +`['1:31']` during February will clamp the actual end date to 28 or 29 depending on leap years. + +`months`: A list of calendar months identified by a case-insentive name (e.g. ‘January’) or by number, +where January = 1. Ranges are also accepted. For example, `['1:3', 'may:august', 'december']` + +`years`: A numerical list of years. Ranges are accepted. For example, `['2020:2022', '2030']` + ## `` An inhibition rule mutes an alert (target) matching a set of matchers From 58c1ef58a1773084ead629c8b3cd8a00cee24f0e Mon Sep 17 00:00:00 2001 From: Ben Ridley Date: Tue, 13 Oct 2020 12:23:42 +1100 Subject: [PATCH 15/47] Clarify boundaries of ranges in docs Signed-off-by: Ben Ridley --- docs/configuration.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index d6e41102c8..96413c3db0 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -261,12 +261,15 @@ of the form : and are inclusive on both ends. For example: Negative values are also accepted which begin at the end of the month, e.g. -1 during January would represent January 31. For example: `['1:5', '-3:-1']`. Extending past the start or end of the month will cause it to be clamped. E.g. specifying -`['1:31']` during February will clamp the actual end date to 28 or 29 depending on leap years. +`['1:31']` during February will clamp the actual end date to 28 or 29 depending on leap years. +Inclusive on both ends. `months`: A list of calendar months identified by a case-insentive name (e.g. ‘January’) or by number, -where January = 1. Ranges are also accepted. For example, `['1:3', 'may:august', 'december']` +where January = 1. Ranges are also accepted. For example, `['1:3', 'may:august', 'december']`. +Inclusive on both ends. -`years`: A numerical list of years. Ranges are accepted. For example, `['2020:2022', '2030']` +`years`: A numerical list of years. Ranges are accepted. For example, `['2020:2022', '2030']`. +Inclusive on both ends. ## `` From 0e9838c8f3822551aa03a8163c1fe01f0f5eda2f Mon Sep 17 00:00:00 2001 From: Ben Ridley Date: Tue, 13 Oct 2020 12:28:36 +1100 Subject: [PATCH 16/47] Fix formatting Signed-off-by: Ben Ridley --- timeinterval/timeinterval_test.go | 860 +++++++++++++++--------------- 1 file changed, 430 insertions(+), 430 deletions(-) diff --git a/timeinterval/timeinterval_test.go b/timeinterval/timeinterval_test.go index 74d2d233bd..25a813614f 100644 --- a/timeinterval/timeinterval_test.go +++ b/timeinterval/timeinterval_test.go @@ -1,430 +1,430 @@ -package timeinterval - -import ( - "reflect" - "testing" - "time" - - "gopkg.in/yaml.v2" -) - -var timeIntervalTestCases = []struct { - validTimeStrings []string - invalidTimeStrings []string - timeInterval TimeInterval -}{ - { - timeInterval: TimeInterval{}, - validTimeStrings: []string{ - "02 Jan 06 15:04 MST", - "03 Jan 07 10:04 MST", - "04 Jan 06 09:04 MST", - }, - invalidTimeStrings: []string{}, - }, - { - // 9am to 5pm, monday to friday - timeInterval: TimeInterval{ - Times: []TimeRange{{StartMinute: 540, EndMinute: 1020}}, - Weekdays: []WeekdayRange{{InclusiveRange{Begin: 1, End: 5}}}, - }, - validTimeStrings: []string{ - "04 May 20 15:04 MST", - "05 May 20 10:04 MST", - "09 Jun 20 09:04 MST", - }, - invalidTimeStrings: []string{ - "03 May 20 15:04 MST", - "04 May 20 08:59 MST", - "05 May 20 05:00 MST", - }, - }, - { - // Easter 2020 - timeInterval: TimeInterval{ - DaysOfMonth: []DayOfMonthRange{{InclusiveRange{Begin: 4, End: 6}}}, - Months: []MonthRange{{InclusiveRange{Begin: 4, End: 4}}}, - Years: []YearRange{{InclusiveRange{Begin: 2020, End: 2020}}}, - }, - validTimeStrings: []string{ - "04 Apr 20 15:04 MST", - "05 Apr 20 00:00 MST", - "06 Apr 20 23:05 MST", - }, - invalidTimeStrings: []string{ - "03 May 18 15:04 MST", - "03 Apr 20 23:59 MST", - "04 Jun 20 23:59 MST", - "06 Apr 19 23:59 MST", - "07 Apr 20 00:00 MST", - }, - }, - { - // Check negative days of month, last 3 days of each month - timeInterval: TimeInterval{ - DaysOfMonth: []DayOfMonthRange{{InclusiveRange{Begin: -3, End: -1}}}, - }, - validTimeStrings: []string{ - "31 Jan 20 15:04 MST", - "30 Jan 20 15:04 MST", - "29 Jan 20 15:04 MST", - "30 Jun 20 00:00 MST", - "29 Feb 20 23:05 MST", - }, - invalidTimeStrings: []string{ - "03 May 18 15:04 MST", - "27 Jan 20 15:04 MST", - "03 Apr 20 23:59 MST", - "04 Jun 20 23:59 MST", - "06 Apr 19 23:59 MST", - "07 Apr 20 00:00 MST", - "01 Mar 20 00:00 MST", - }, - }, - { - // Check out of bound days are clamped to month boundaries - timeInterval: TimeInterval{ - Months: []MonthRange{{InclusiveRange{Begin: 6, End: 6}}}, - DaysOfMonth: []DayOfMonthRange{{InclusiveRange{Begin: -31, End: 31}}}, - }, - validTimeStrings: []string{ - "30 Jun 20 00:00 MST", - "01 Jun 20 00:00 MST", - }, - invalidTimeStrings: []string{ - "31 May 20 00:00 MST", - "1 Jul 20 00:00 MST", - }, - }, -} - -var timeStringTestCases = []struct { - timeString string - TimeRange TimeRange - expectError bool -}{ - { - timeString: "{'start_time': '00:00', 'end_time': '24:00'}", - TimeRange: TimeRange{StartMinute: 0, EndMinute: 1440}, - expectError: false, - }, - { - timeString: "{'start_time': '01:35', 'end_time': '17:39'}", - TimeRange: TimeRange{StartMinute: 95, EndMinute: 1059}, - expectError: false, - }, - { - timeString: "{'start_time': '09:35', 'end_time': '09:39'}", - TimeRange: TimeRange{StartMinute: 575, EndMinute: 579}, - expectError: false, - }, - { - // Error: Begin and End times are the same - timeString: "{'start_time': '17:31', 'end_time': '17:31'}", - TimeRange: TimeRange{}, - expectError: true, - }, - { - // Error: End time out of range - timeString: "{'start_time': '12:30', 'end_time': '24:01'}", - TimeRange: TimeRange{}, - expectError: true, - }, - { - // Error: Start time greater than End time - timeString: "{'start_time': '09:30', 'end_time': '07:41'}", - TimeRange: TimeRange{}, - expectError: true, - }, - { - // Error: Start time out of range and greater than End time - timeString: "{'start_time': '24:00', 'end_time': '17:41'}", - TimeRange: TimeRange{}, - expectError: true, - }, - { - // Error: No range specified - timeString: "{'start_time': '14:03'}", - TimeRange: TimeRange{}, - expectError: true, - }, -} - -var dayOfWeekStringTestCases = []struct { - dowString string - ranges []WeekdayRange - expectError bool -}{ - { - dowString: "['monday:friday', 'saturday']", - ranges: []WeekdayRange{{InclusiveRange{Begin: 1, End: 5}}, {InclusiveRange{Begin: 6, End: 6}}}, - expectError: false, - }, -} - -var yamlUnmarshalTestCases = []struct { - in string - intervals []TimeInterval - contains []string - excludes []string - expectError bool -}{ - { - // Simple business hours test - in: ` ---- -- weekdays: ['monday:friday'] - times: - - start_time: '09:00' - end_time: '17:00' -`, - intervals: []TimeInterval{ - { - Weekdays: []WeekdayRange{{InclusiveRange{Begin: 1, End: 5}}}, - Times: []TimeRange{{StartMinute: 540, EndMinute: 1020}}, - }, - }, - contains: []string{ - "08 Jul 20 09:00 MST", - "08 Jul 20 16:59 MST", - }, - excludes: []string{ - "08 Jul 20 05:00 MST", - "08 Jul 20 08:59 MST", - }, - expectError: false, - }, - { - // More advanced test with negative indices and ranges - in: ` ---- - # Last week, excluding Saturday, of the first quarter of the year during business hours from 2020 to 2025 and 2030-2035 -- weekdays: ['monday:friday', 'sunday'] - months: ['january:march'] - days_of_month: ['-7:-1'] - years: ['2020:2025', '2030:2035'] - times: - - start_time: '09:00' - end_time: '17:00' -`, - intervals: []TimeInterval{ - { - Weekdays: []WeekdayRange{{InclusiveRange{Begin: 1, End: 5}}, {InclusiveRange{Begin: 0, End: 0}}}, - Times: []TimeRange{{StartMinute: 540, EndMinute: 1020}}, - Months: []MonthRange{{InclusiveRange{1, 3}}}, - DaysOfMonth: []DayOfMonthRange{{InclusiveRange{-7, -1}}}, - Years: []YearRange{{InclusiveRange{2020, 2025}}, {InclusiveRange{2030, 2035}}}, - }, - }, - contains: []string{ - "27 Jan 21 09:00 MST", - "28 Jan 21 16:59 MST", - "29 Jan 21 13:00 MST", - "31 Mar 25 13:00 MST", - "31 Mar 25 13:00 MST", - "31 Jan 35 13:00 MST", - }, - excludes: []string{ - "30 Jan 21 13:00 MST", // Saturday - "01 Apr 21 13:00 MST", // 4th month - "30 Jan 26 13:00 MST", // 2026 - "31 Jan 35 17:01 MST", // After 5pm - }, - expectError: false, - }, - { - in: ` ---- -- weekdays: ['monday:friday'] - times: - - start_time: '09:00' - end_time: '17:00'`, - intervals: []TimeInterval{ - { - Weekdays: []WeekdayRange{{InclusiveRange{Begin: 1, End: 5}}}, - Times: []TimeRange{{StartMinute: 540, EndMinute: 1020}}, - }, - }, - contains: []string{ - "01 Apr 21 13:00 GMT", - }, - }, - { - // Start day before End day - in: ` ---- -- weekdays: ['friday:monday']`, - expectError: true, - }, - { - // Invalid weekdays - in: ` ---- -- weekdays: ['blurgsday:flurgsday'] -`, - expectError: true, - }, - { - // 0 day of month - in: ` ---- -- days_of_month: ['0'] -`, - expectError: true, - }, - { - // Too early day of month - in: ` ---- -- days_of_month: ['-50:-20'] -`, - expectError: true, - }, - { - // Negative indices should work - in: ` ---- -- days_of_month: ['1:-1'] -`, - intervals: []TimeInterval{ - { - DaysOfMonth: []DayOfMonthRange{{InclusiveRange{1, -1}}}, - }, - }, - expectError: false, - }, - { - // Negative start date before positive End date - in: ` ---- -- days_of_month: ['-15:5'] -`, - expectError: true, - }, - { - // Negative End date before positive postive start date - in: ` ---- -- days_of_month: ['10:-25'] -`, - expectError: true, - }, -} - -func TestYamlUnmarshal(t *testing.T) { - for _, tc := range yamlUnmarshalTestCases { - var ti []TimeInterval - err := yaml.Unmarshal([]byte(tc.in), &ti) - if err != nil && !tc.expectError { - t.Errorf("Received unexpected error: %v when parsing %v", err, tc.in) - } else if err == nil && tc.expectError { - t.Errorf("Expected error when unmarshalling %s but didn't receive one", tc.in) - } else if err != nil && tc.expectError { - continue - } - if !reflect.DeepEqual(ti, tc.intervals) { - t.Errorf("Error unmarshalling %s: Want %+v, got %+v", tc.in, tc.intervals, ti) - } - for _, ts := range tc.contains { - _t, _ := time.Parse(time.RFC822, ts) - isContained := false - for _, interval := range ti { - if interval.ContainsTime(_t) { - isContained = true - } - } - if !isContained { - t.Errorf("Expected intervals to contain time %s", _t) - } - } - for _, ts := range tc.excludes { - _t, _ := time.Parse(time.RFC822, ts) - isContained := false - for _, interval := range ti { - if interval.ContainsTime(_t) { - isContained = true - } - } - if isContained { - t.Errorf("Expected intervals to exclude time %s", _t) - } - } - } -} - -func TestContainsTime(t *testing.T) { - for _, tc := range timeIntervalTestCases { - for _, ts := range tc.validTimeStrings { - _t, _ := time.Parse(time.RFC822, ts) - if !tc.timeInterval.ContainsTime(_t) { - t.Errorf("Expected period %+v to contain %+v", tc.timeInterval, _t) - } - } - for _, ts := range tc.invalidTimeStrings { - _t, _ := time.Parse(time.RFC822, ts) - if tc.timeInterval.ContainsTime(_t) { - t.Errorf("Period %+v not expected to contain %+v", tc.timeInterval, _t) - } - } - } -} - -func TestParseTimeString(t *testing.T) { - for _, tc := range timeStringTestCases { - var tr TimeRange - err := yaml.Unmarshal([]byte(tc.timeString), &tr) - if err != nil && !tc.expectError { - t.Errorf("Received unexpected error: %v when parsing %v", err, tc.timeString) - } else if err == nil && tc.expectError { - t.Errorf("Expected error for invalid string %s but didn't receive one", tc.timeString) - } else if !reflect.DeepEqual(tr, tc.TimeRange) { - t.Errorf("Error parsing time string %s: Want %+v, got %+v", tc.timeString, tc.TimeRange, tr) - } - } -} - -func TestParseWeek(t *testing.T) { - for _, tc := range dayOfWeekStringTestCases { - var wr []WeekdayRange - err := yaml.Unmarshal([]byte(tc.dowString), &wr) - if err != nil && !tc.expectError { - t.Errorf("Received unexpected error: %v when parsing %v", err, tc.dowString) - } else if err == nil && tc.expectError { - t.Errorf("Expected error for invalid string %s but didn't receive one", tc.dowString) - } else if !reflect.DeepEqual(wr, tc.ranges) { - t.Errorf("Error parsing time string %s: Want %+v, got %+v", tc.dowString, tc.ranges, wr) - } - } -} - -func TestYamlMarshal(t *testing.T) { - for _, tc := range yamlUnmarshalTestCases { - if tc.expectError { - continue - } - var ti []TimeInterval - err := yaml.Unmarshal([]byte(tc.in), &ti) - if err != nil { - t.Error(err) - } - out, err := yaml.Marshal(&ti) - if err != nil { - t.Error(err) - } - var ti2 []TimeInterval - yaml.Unmarshal(out, &ti2) - if !reflect.DeepEqual(ti, ti2) { - t.Errorf("Re-marshalling %s produced a different TimeInterval", tc.in) - } - } -} - -func emptyInterval() TimeInterval { - return TimeInterval{ - Times: []TimeRange{}, - Weekdays: []WeekdayRange{}, - DaysOfMonth: []DayOfMonthRange{}, - Months: []MonthRange{}, - Years: []YearRange{}, - } -} +package timeinterval + +import ( + "reflect" + "testing" + "time" + + "gopkg.in/yaml.v2" +) + +var timeIntervalTestCases = []struct { + validTimeStrings []string + invalidTimeStrings []string + timeInterval TimeInterval +}{ + { + timeInterval: TimeInterval{}, + validTimeStrings: []string{ + "02 Jan 06 15:04 MST", + "03 Jan 07 10:04 MST", + "04 Jan 06 09:04 MST", + }, + invalidTimeStrings: []string{}, + }, + { + // 9am to 5pm, monday to friday + timeInterval: TimeInterval{ + Times: []TimeRange{{StartMinute: 540, EndMinute: 1020}}, + Weekdays: []WeekdayRange{{InclusiveRange{Begin: 1, End: 5}}}, + }, + validTimeStrings: []string{ + "04 May 20 15:04 MST", + "05 May 20 10:04 MST", + "09 Jun 20 09:04 MST", + }, + invalidTimeStrings: []string{ + "03 May 20 15:04 MST", + "04 May 20 08:59 MST", + "05 May 20 05:00 MST", + }, + }, + { + // Easter 2020 + timeInterval: TimeInterval{ + DaysOfMonth: []DayOfMonthRange{{InclusiveRange{Begin: 4, End: 6}}}, + Months: []MonthRange{{InclusiveRange{Begin: 4, End: 4}}}, + Years: []YearRange{{InclusiveRange{Begin: 2020, End: 2020}}}, + }, + validTimeStrings: []string{ + "04 Apr 20 15:04 MST", + "05 Apr 20 00:00 MST", + "06 Apr 20 23:05 MST", + }, + invalidTimeStrings: []string{ + "03 May 18 15:04 MST", + "03 Apr 20 23:59 MST", + "04 Jun 20 23:59 MST", + "06 Apr 19 23:59 MST", + "07 Apr 20 00:00 MST", + }, + }, + { + // Check negative days of month, last 3 days of each month + timeInterval: TimeInterval{ + DaysOfMonth: []DayOfMonthRange{{InclusiveRange{Begin: -3, End: -1}}}, + }, + validTimeStrings: []string{ + "31 Jan 20 15:04 MST", + "30 Jan 20 15:04 MST", + "29 Jan 20 15:04 MST", + "30 Jun 20 00:00 MST", + "29 Feb 20 23:05 MST", + }, + invalidTimeStrings: []string{ + "03 May 18 15:04 MST", + "27 Jan 20 15:04 MST", + "03 Apr 20 23:59 MST", + "04 Jun 20 23:59 MST", + "06 Apr 19 23:59 MST", + "07 Apr 20 00:00 MST", + "01 Mar 20 00:00 MST", + }, + }, + { + // Check out of bound days are clamped to month boundaries + timeInterval: TimeInterval{ + Months: []MonthRange{{InclusiveRange{Begin: 6, End: 6}}}, + DaysOfMonth: []DayOfMonthRange{{InclusiveRange{Begin: -31, End: 31}}}, + }, + validTimeStrings: []string{ + "30 Jun 20 00:00 MST", + "01 Jun 20 00:00 MST", + }, + invalidTimeStrings: []string{ + "31 May 20 00:00 MST", + "1 Jul 20 00:00 MST", + }, + }, +} + +var timeStringTestCases = []struct { + timeString string + TimeRange TimeRange + expectError bool +}{ + { + timeString: "{'start_time': '00:00', 'end_time': '24:00'}", + TimeRange: TimeRange{StartMinute: 0, EndMinute: 1440}, + expectError: false, + }, + { + timeString: "{'start_time': '01:35', 'end_time': '17:39'}", + TimeRange: TimeRange{StartMinute: 95, EndMinute: 1059}, + expectError: false, + }, + { + timeString: "{'start_time': '09:35', 'end_time': '09:39'}", + TimeRange: TimeRange{StartMinute: 575, EndMinute: 579}, + expectError: false, + }, + { + // Error: Begin and End times are the same + timeString: "{'start_time': '17:31', 'end_time': '17:31'}", + TimeRange: TimeRange{}, + expectError: true, + }, + { + // Error: End time out of range + timeString: "{'start_time': '12:30', 'end_time': '24:01'}", + TimeRange: TimeRange{}, + expectError: true, + }, + { + // Error: Start time greater than End time + timeString: "{'start_time': '09:30', 'end_time': '07:41'}", + TimeRange: TimeRange{}, + expectError: true, + }, + { + // Error: Start time out of range and greater than End time + timeString: "{'start_time': '24:00', 'end_time': '17:41'}", + TimeRange: TimeRange{}, + expectError: true, + }, + { + // Error: No range specified + timeString: "{'start_time': '14:03'}", + TimeRange: TimeRange{}, + expectError: true, + }, +} + +var dayOfWeekStringTestCases = []struct { + dowString string + ranges []WeekdayRange + expectError bool +}{ + { + dowString: "['monday:friday', 'saturday']", + ranges: []WeekdayRange{{InclusiveRange{Begin: 1, End: 5}}, {InclusiveRange{Begin: 6, End: 6}}}, + expectError: false, + }, +} + +var yamlUnmarshalTestCases = []struct { + in string + intervals []TimeInterval + contains []string + excludes []string + expectError bool +}{ + { + // Simple business hours test + in: ` +--- +- weekdays: ['monday:friday'] + times: + - start_time: '09:00' + end_time: '17:00' +`, + intervals: []TimeInterval{ + { + Weekdays: []WeekdayRange{{InclusiveRange{Begin: 1, End: 5}}}, + Times: []TimeRange{{StartMinute: 540, EndMinute: 1020}}, + }, + }, + contains: []string{ + "08 Jul 20 09:00 MST", + "08 Jul 20 16:59 MST", + }, + excludes: []string{ + "08 Jul 20 05:00 MST", + "08 Jul 20 08:59 MST", + }, + expectError: false, + }, + { + // More advanced test with negative indices and ranges + in: ` +--- + # Last week, excluding Saturday, of the first quarter of the year during business hours from 2020 to 2025 and 2030-2035 +- weekdays: ['monday:friday', 'sunday'] + months: ['january:march'] + days_of_month: ['-7:-1'] + years: ['2020:2025', '2030:2035'] + times: + - start_time: '09:00' + end_time: '17:00' +`, + intervals: []TimeInterval{ + { + Weekdays: []WeekdayRange{{InclusiveRange{Begin: 1, End: 5}}, {InclusiveRange{Begin: 0, End: 0}}}, + Times: []TimeRange{{StartMinute: 540, EndMinute: 1020}}, + Months: []MonthRange{{InclusiveRange{1, 3}}}, + DaysOfMonth: []DayOfMonthRange{{InclusiveRange{-7, -1}}}, + Years: []YearRange{{InclusiveRange{2020, 2025}}, {InclusiveRange{2030, 2035}}}, + }, + }, + contains: []string{ + "27 Jan 21 09:00 MST", + "28 Jan 21 16:59 MST", + "29 Jan 21 13:00 MST", + "31 Mar 25 13:00 MST", + "31 Mar 25 13:00 MST", + "31 Jan 35 13:00 MST", + }, + excludes: []string{ + "30 Jan 21 13:00 MST", // Saturday + "01 Apr 21 13:00 MST", // 4th month + "30 Jan 26 13:00 MST", // 2026 + "31 Jan 35 17:01 MST", // After 5pm + }, + expectError: false, + }, + { + in: ` +--- +- weekdays: ['monday:friday'] + times: + - start_time: '09:00' + end_time: '17:00'`, + intervals: []TimeInterval{ + { + Weekdays: []WeekdayRange{{InclusiveRange{Begin: 1, End: 5}}}, + Times: []TimeRange{{StartMinute: 540, EndMinute: 1020}}, + }, + }, + contains: []string{ + "01 Apr 21 13:00 GMT", + }, + }, + { + // Start day before End day + in: ` +--- +- weekdays: ['friday:monday']`, + expectError: true, + }, + { + // Invalid weekdays + in: ` +--- +- weekdays: ['blurgsday:flurgsday'] +`, + expectError: true, + }, + { + // 0 day of month + in: ` +--- +- days_of_month: ['0'] +`, + expectError: true, + }, + { + // Too early day of month + in: ` +--- +- days_of_month: ['-50:-20'] +`, + expectError: true, + }, + { + // Negative indices should work + in: ` +--- +- days_of_month: ['1:-1'] +`, + intervals: []TimeInterval{ + { + DaysOfMonth: []DayOfMonthRange{{InclusiveRange{1, -1}}}, + }, + }, + expectError: false, + }, + { + // Negative start date before positive End date + in: ` +--- +- days_of_month: ['-15:5'] +`, + expectError: true, + }, + { + // Negative End date before positive postive start date + in: ` +--- +- days_of_month: ['10:-25'] +`, + expectError: true, + }, +} + +func TestYamlUnmarshal(t *testing.T) { + for _, tc := range yamlUnmarshalTestCases { + var ti []TimeInterval + err := yaml.Unmarshal([]byte(tc.in), &ti) + if err != nil && !tc.expectError { + t.Errorf("Received unexpected error: %v when parsing %v", err, tc.in) + } else if err == nil && tc.expectError { + t.Errorf("Expected error when unmarshalling %s but didn't receive one", tc.in) + } else if err != nil && tc.expectError { + continue + } + if !reflect.DeepEqual(ti, tc.intervals) { + t.Errorf("Error unmarshalling %s: Want %+v, got %+v", tc.in, tc.intervals, ti) + } + for _, ts := range tc.contains { + _t, _ := time.Parse(time.RFC822, ts) + isContained := false + for _, interval := range ti { + if interval.ContainsTime(_t) { + isContained = true + } + } + if !isContained { + t.Errorf("Expected intervals to contain time %s", _t) + } + } + for _, ts := range tc.excludes { + _t, _ := time.Parse(time.RFC822, ts) + isContained := false + for _, interval := range ti { + if interval.ContainsTime(_t) { + isContained = true + } + } + if isContained { + t.Errorf("Expected intervals to exclude time %s", _t) + } + } + } +} + +func TestContainsTime(t *testing.T) { + for _, tc := range timeIntervalTestCases { + for _, ts := range tc.validTimeStrings { + _t, _ := time.Parse(time.RFC822, ts) + if !tc.timeInterval.ContainsTime(_t) { + t.Errorf("Expected period %+v to contain %+v", tc.timeInterval, _t) + } + } + for _, ts := range tc.invalidTimeStrings { + _t, _ := time.Parse(time.RFC822, ts) + if tc.timeInterval.ContainsTime(_t) { + t.Errorf("Period %+v not expected to contain %+v", tc.timeInterval, _t) + } + } + } +} + +func TestParseTimeString(t *testing.T) { + for _, tc := range timeStringTestCases { + var tr TimeRange + err := yaml.Unmarshal([]byte(tc.timeString), &tr) + if err != nil && !tc.expectError { + t.Errorf("Received unexpected error: %v when parsing %v", err, tc.timeString) + } else if err == nil && tc.expectError { + t.Errorf("Expected error for invalid string %s but didn't receive one", tc.timeString) + } else if !reflect.DeepEqual(tr, tc.TimeRange) { + t.Errorf("Error parsing time string %s: Want %+v, got %+v", tc.timeString, tc.TimeRange, tr) + } + } +} + +func TestParseWeek(t *testing.T) { + for _, tc := range dayOfWeekStringTestCases { + var wr []WeekdayRange + err := yaml.Unmarshal([]byte(tc.dowString), &wr) + if err != nil && !tc.expectError { + t.Errorf("Received unexpected error: %v when parsing %v", err, tc.dowString) + } else if err == nil && tc.expectError { + t.Errorf("Expected error for invalid string %s but didn't receive one", tc.dowString) + } else if !reflect.DeepEqual(wr, tc.ranges) { + t.Errorf("Error parsing time string %s: Want %+v, got %+v", tc.dowString, tc.ranges, wr) + } + } +} + +func TestYamlMarshal(t *testing.T) { + for _, tc := range yamlUnmarshalTestCases { + if tc.expectError { + continue + } + var ti []TimeInterval + err := yaml.Unmarshal([]byte(tc.in), &ti) + if err != nil { + t.Error(err) + } + out, err := yaml.Marshal(&ti) + if err != nil { + t.Error(err) + } + var ti2 []TimeInterval + yaml.Unmarshal(out, &ti2) + if !reflect.DeepEqual(ti, ti2) { + t.Errorf("Re-marshalling %s produced a different TimeInterval", tc.in) + } + } +} + +func emptyInterval() TimeInterval { + return TimeInterval{ + Times: []TimeRange{}, + Weekdays: []WeekdayRange{}, + DaysOfMonth: []DayOfMonthRange{}, + Months: []MonthRange{}, + Years: []YearRange{}, + } +} From e052540c92ef82939d03f18c8f2d829773513b4b Mon Sep 17 00:00:00 2001 From: Ben Ridley Date: Tue, 13 Oct 2020 12:30:47 +1100 Subject: [PATCH 17/47] Add license header Signed-off-by: Ben Ridley --- timeinterval/timeinterval.go | 13 +++++++++++++ timeinterval/timeinterval_test.go | 13 +++++++++++++ 2 files changed, 26 insertions(+) diff --git a/timeinterval/timeinterval.go b/timeinterval/timeinterval.go index 54cfa6d17a..9db043ae53 100644 --- a/timeinterval/timeinterval.go +++ b/timeinterval/timeinterval.go @@ -1,3 +1,16 @@ +// Copyright 2020 Prometheus Team +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package timeinterval import ( diff --git a/timeinterval/timeinterval_test.go b/timeinterval/timeinterval_test.go index 25a813614f..a93b1cc097 100644 --- a/timeinterval/timeinterval_test.go +++ b/timeinterval/timeinterval_test.go @@ -1,3 +1,16 @@ +// Copyright 2020 Prometheus Team +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package timeinterval import ( From 3ece40664bf66992da5c49db3c10473ff501c244 Mon Sep 17 00:00:00 2001 From: Ben Ridley Date: Tue, 13 Oct 2020 12:32:37 +1100 Subject: [PATCH 18/47] Remove unused test function Signed-off-by: Ben Ridley --- timeinterval/timeinterval_test.go | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/timeinterval/timeinterval_test.go b/timeinterval/timeinterval_test.go index a93b1cc097..0794b51c04 100644 --- a/timeinterval/timeinterval_test.go +++ b/timeinterval/timeinterval_test.go @@ -431,13 +431,3 @@ func TestYamlMarshal(t *testing.T) { } } } - -func emptyInterval() TimeInterval { - return TimeInterval{ - Times: []TimeRange{}, - Weekdays: []WeekdayRange{}, - DaysOfMonth: []DayOfMonthRange{}, - Months: []MonthRange{}, - Years: []YearRange{}, - } -} From 44e9aa97622fbc9dc89b568ad8ce95871222f594 Mon Sep 17 00:00:00 2001 From: Ben Ridley Date: Wed, 14 Oct 2020 21:20:28 +1100 Subject: [PATCH 19/47] Change undefined route to use quoted string formatting Signed-off-by: Ben Ridley --- config/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.go b/config/config.go index 5d127f720e..bf3a934038 100644 --- a/config/config.go +++ b/config/config.go @@ -463,7 +463,7 @@ func checkTimeInterval(r *Route, timeIntervals map[string]struct{}) error { } for _, mt := range r.MuteTimes { if _, ok := timeIntervals[mt]; !ok { - return fmt.Errorf("undefined time interval %s used in route", mt) + return fmt.Errorf("undefined time interval %q used in route", mt) } } return nil From ad385c275fc58a8c71c4eb3bae48677bda70f1de Mon Sep 17 00:00:00 2001 From: Ben Ridley Date: Wed, 14 Oct 2020 21:26:16 +1100 Subject: [PATCH 20/47] Add check for undefined name in a mute time interval Signed-off-by: Ben Ridley --- config/config.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/config/config.go b/config/config.go index bf3a934038..128bff5cad 100644 --- a/config/config.go +++ b/config/config.go @@ -226,6 +226,18 @@ type MuteTimeInterval struct { TimeIntervals []timeinterval.TimeInterval `yaml:"time_intervals"` } +// UnmarshalYAML implements the yaml.Unmarshaler interface for MuteTimeInterval +func (mt *MuteTimeInterval) UnmarshalYAML(unmarshal func(interface{}) error) error { + type plain MuteTimeInterval + if err := unmarshal((*plain)(mt)); err != nil { + return err + } + if mt.Name == "" { + return fmt.Errorf("missing name in mute time interval") + } + return nil +} + // Config is the top-level configuration for Alertmanager's config files. type Config struct { Global *GlobalConfig `yaml:"global,omitempty" json:"global,omitempty"` From dd7a35e2b8f60806cd1185a9e5106d2bb2d20794 Mon Sep 17 00:00:00 2001 From: Ben Ridley Date: Wed, 14 Oct 2020 21:26:55 +1100 Subject: [PATCH 21/47] Add tests for configuration of mute times Signed-off-by: Ben Ridley --- config/config_test.go | 89 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/config/config_test.go b/config/config_test.go index 946fef9307..fd4afae17a 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -151,6 +151,65 @@ receivers: } +func TestMuteTimeExists(t *testing.T) { + in := ` +route: + receiver: team-Y + routes: + - match: + severity: critical + mute_times: + - business_hours + +receivers: +- name: 'team-Y' +` + _, err := Load(in) + + expected := "undefined time interval \"business_hours\" used in route" + + if err == nil { + t.Fatalf("no error returned, expected:\n%q", expected) + } + if err.Error() != expected { + t.Errorf("\nexpected:\n%q\ngot:\n%q", expected, err.Error()) + } + +} + +func TestMuteTimeHasName(t *testing.T) { + in := ` +mute_time_intervals: +- name: + time_intervals: + - times: + - start_time: '09:00' + end_time: '17:00' + +receivers: +- name: 'team-X-mails' + +route: + receiver: 'team-X-mails' + routes: + - match: + severity: critical + mute_times: + - business_hours +` + _, err := Load(in) + + expected := "missing name in mute time interval" + + if err == nil { + t.Fatalf("no error returned, expected:\n%q", expected) + } + if err.Error() != expected { + t.Errorf("\nexpected:\n%q\ngot:\n%q", expected, err.Error()) + } + +} + func TestGroupByHasNoDuplicatedLabels(t *testing.T) { in := ` route: @@ -231,6 +290,36 @@ receivers: } +func TestRootRouteNoMuteTimes(t *testing.T) { + in := ` +mute_time_intervals: +- name: my_mute_time + time_intervals: + - times: + - start_time: '09:00' + end_time: '17:00' + +receivers: +- name: 'team-X-mails' + +route: + receiver: 'team-X-mails' + mute_times: + - my_mute_time +` + _, err := Load(in) + + expected := "root route cannot have any mute times" + + if err == nil { + t.Fatalf("no error returned, expected:\n%q", expected) + } + if err.Error() != expected { + t.Errorf("\nexpected:\n%q\ngot:\n%q", expected, err.Error()) + } + +} + func TestRootRouteHasNoMatcher(t *testing.T) { in := ` route: From 4c881ef5fbdbddcad12c62a65a2ec5bfb3fdb83b Mon Sep 17 00:00:00 2001 From: Ben Ridley Date: Wed, 14 Oct 2020 21:29:50 +1100 Subject: [PATCH 22/47] Tidy up error message formatting Signed-off-by: Ben Ridley --- timeinterval/timeinterval.go | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/timeinterval/timeinterval.go b/timeinterval/timeinterval.go index 9db043ae53..bf81d94bd7 100644 --- a/timeinterval/timeinterval.go +++ b/timeinterval/timeinterval.go @@ -170,7 +170,7 @@ func (r *WeekdayRange) UnmarshalYAML(unmarshal func(interface{}) error) error { } err := stringableRangeFromString(str, r) if r.Begin > r.End { - return errors.New("Start day cannot be before End day") + return errors.New("start day cannot be before end day") } if r.Begin < 0 || r.Begin > 6 { return fmt.Errorf("%s is not a valid day of the week: out of range", str) @@ -185,14 +185,14 @@ func (r *WeekdayRange) UnmarshalYAML(unmarshal func(interface{}) error) error { func (r WeekdayRange) MarshalYAML() (interface{}, error) { beginStr, ok := daysOfWeekInv[r.Begin] if !ok { - return nil, fmt.Errorf("Unable to convert %d into weekday string", r.Begin) + return nil, fmt.Errorf("unable to convert %d into weekday string", r.Begin) } if r.Begin == r.End { return interface{}(beginStr), nil } endStr, ok := daysOfWeekInv[r.End] if !ok { - return nil, fmt.Errorf("Unable to convert %d into weekday string", r.End) + return nil, fmt.Errorf("unable to convert %d into weekday string", r.End) } rangeStr := fmt.Sprintf("%s:%s", beginStr, endStr) return interface{}(rangeStr), nil @@ -221,7 +221,7 @@ func (r *DayOfMonthRange) UnmarshalYAML(unmarshal func(interface{}) error) error trueEnd = 30 + r.End } if trueBegin > trueEnd { - return errors.New("Start day cannot be before End day") + return errors.New("start day cannot be before end day") } return err } @@ -234,7 +234,7 @@ func (r *MonthRange) UnmarshalYAML(unmarshal func(interface{}) error) error { } err := stringableRangeFromString(str, r) if r.Begin > r.End { - return errors.New("Start month cannot be before End month") + return errors.New("start month cannot be before end month") } if r.Begin < 1 || r.Begin > 12 { return fmt.Errorf("%s is not a valid month: out of range", str) @@ -249,14 +249,14 @@ func (r *MonthRange) UnmarshalYAML(unmarshal func(interface{}) error) error { func (r MonthRange) MarshalYAML() (interface{}, error) { beginStr, ok := monthsInv[r.Begin] if !ok { - return nil, fmt.Errorf("Unable to convert %d into month", r.Begin) + return nil, fmt.Errorf("unable to convert %d into month", r.Begin) } if r.Begin == r.End { return interface{}(beginStr), nil } endStr, ok := monthsInv[r.End] if !ok { - return nil, fmt.Errorf("Unable to convert %d into month", r.End) + return nil, fmt.Errorf("unable to convert %d into month", r.End) } rangeStr := fmt.Sprintf("%s:%s", beginStr, endStr) return interface{}(rangeStr), nil @@ -270,7 +270,7 @@ func (r *YearRange) UnmarshalYAML(unmarshal func(interface{}) error) error { } err := stringableRangeFromString(str, r) if r.Begin > r.End { - return errors.New("Start day cannot be before End day") + return errors.New("start day cannot be before end day") } return err } @@ -282,7 +282,7 @@ func (tr *TimeRange) UnmarshalYAML(unmarshal func(interface{}) error) error { return err } if y.EndTime == "" || y.StartTime == "" { - return errors.New("Both start and End times must be provided") + return errors.New("both start and end times must be provided") } start, err := parseTime(y.StartTime) if err != nil { @@ -293,13 +293,13 @@ func (tr *TimeRange) UnmarshalYAML(unmarshal func(interface{}) error) error { return err } if start < 0 { - return errors.New("Start time out of range") + return errors.New("start time out of range") } if End > 1440 { return errors.New("End time out of range") } if start >= End { - return errors.New("Start time cannot be equal or greater than End time") + return errors.New("start time cannot be equal or greater than end time") } tr.StartMinute, tr.EndMinute = start, End return nil @@ -435,11 +435,11 @@ func (tp TimeInterval) ContainsTime(t time.Time) bool { // Converts a string of the form "HH:MM" into a TimeRange func parseTime(in string) (mins int, err error) { if !validTimeRE.MatchString(in) { - return 0, fmt.Errorf("Couldn't parse timestamp %s, invalid format", in) + return 0, fmt.Errorf("couldn't parse timestamp %s, invalid format", in) } timestampComponents := strings.Split(in, ":") if len(timestampComponents) != 2 { - return 0, fmt.Errorf("Invalid timestamp format: %s", in) + return 0, fmt.Errorf("invalid timestamp format: %s", in) } timeStampHours, err := strconv.Atoi(timestampComponents[0]) if err != nil { @@ -450,7 +450,7 @@ func parseTime(in string) (mins int, err error) { return 0, err } if timeStampHours < 0 || timeStampHours > 24 || timeStampMinutes < 0 || timeStampMinutes > 60 { - return 0, fmt.Errorf("Timestamp %s out of range", in) + return 0, fmt.Errorf("timestamp %s out of range", in) } // Timestamps are stored as minutes elapsed in the day, so multiply hours by 60 mins = timeStampHours*60 + timeStampMinutes @@ -463,7 +463,7 @@ func stringableRangeFromString(in string, r stringableRange) (err error) { if strings.ContainsRune(in, ':') { components := strings.Split(in, ":") if len(components) != 2 { - return fmt.Errorf("Coudn't parse range %s, invalid format", in) + return fmt.Errorf("couldn't parse range %s, invalid format", in) } start, err := r.memberFromString(components[0]) if err != nil { From adf94c3168217f8634236bd34c19906ed17038f1 Mon Sep 17 00:00:00 2001 From: Ben Ridley Date: Sun, 15 Nov 2020 16:56:46 +1100 Subject: [PATCH 23/47] Correct errors, test for specific error messages, improve formatting Signed-off-by: Ben Ridley --- timeinterval/timeinterval.go | 106 +++++++++++++----------------- timeinterval/timeinterval_test.go | 81 ++++++++++++++++++++++- 2 files changed, 122 insertions(+), 65 deletions(-) diff --git a/timeinterval/timeinterval.go b/timeinterval/timeinterval.go index bf81d94bd7..0870785467 100644 --- a/timeinterval/timeinterval.go +++ b/timeinterval/timeinterval.go @@ -181,23 +181,6 @@ func (r *WeekdayRange) UnmarshalYAML(unmarshal func(interface{}) error) error { return err } -// MarshalYAML implements the yaml.Marshaler interface for WeekdayRange -func (r WeekdayRange) MarshalYAML() (interface{}, error) { - beginStr, ok := daysOfWeekInv[r.Begin] - if !ok { - return nil, fmt.Errorf("unable to convert %d into weekday string", r.Begin) - } - if r.Begin == r.End { - return interface{}(beginStr), nil - } - endStr, ok := daysOfWeekInv[r.End] - if !ok { - return nil, fmt.Errorf("unable to convert %d into weekday string", r.End) - } - rangeStr := fmt.Sprintf("%s:%s", beginStr, endStr) - return interface{}(rangeStr), nil -} - // UnmarshalYAML implements the Unmarshaller interface for DayOfMonthRange. func (r *DayOfMonthRange) UnmarshalYAML(unmarshal func(interface{}) error) error { var str string @@ -205,23 +188,30 @@ func (r *DayOfMonthRange) UnmarshalYAML(unmarshal func(interface{}) error) error return err } err := stringableRangeFromString(str, r) + // Check beginning <= end accounting for negatives day of month indices as well. + // Months != 31 days can't be addressed here and are clamped, but at least we can catch blatant errors. if r.Begin == 0 || r.Begin < -31 || r.Begin > 31 { return fmt.Errorf("%d is not a valid day of the month: out of range", r.Begin) } if r.End == 0 || r.End < -31 || r.End > 31 { return fmt.Errorf("%d is not a valid day of the month: out of range", r.End) } - // Check Beginning <= End accounting for negatives day of month indices - trueBegin := r.Begin - trueEnd := r.End + // Restricting here prevents errors where begin > end in longer months but not shorter months. + if r.Begin < 0 && r.End > 0 { + return fmt.Errorf("end day must be negative if start day is negative") + } + // Check begin <= end. We can't know this for sure when using negative indices + // but we can prevent cases where its always invalid (using 28 day minimum length) + checkBegin := r.Begin + checkEnd := r.End if r.Begin < 0 { - trueBegin = 30 + r.Begin + checkBegin = 28 + r.Begin } if r.End < 0 { - trueEnd = 30 + r.End + checkEnd = 28 + r.End } - if trueBegin > trueEnd { - return errors.New("start day cannot be before end day") + if checkBegin > checkEnd { + return fmt.Errorf("end day %d is always before start day %d", r.End, r.Begin) } return err } @@ -232,34 +222,15 @@ func (r *MonthRange) UnmarshalYAML(unmarshal func(interface{}) error) error { if err := unmarshal(&str); err != nil { return err } - err := stringableRangeFromString(str, r) - if r.Begin > r.End { - return errors.New("start month cannot be before end month") - } - if r.Begin < 1 || r.Begin > 12 { - return fmt.Errorf("%s is not a valid month: out of range", str) - } - if r.End < 1 || r.End > 12 { - return fmt.Errorf("%s is not a valid month: out of range", str) - } - return err -} - -// MarshalYAML implements the yaml.Marshaler interface for DayOfMonthRange -func (r MonthRange) MarshalYAML() (interface{}, error) { - beginStr, ok := monthsInv[r.Begin] - if !ok { - return nil, fmt.Errorf("unable to convert %d into month", r.Begin) - } - if r.Begin == r.End { - return interface{}(beginStr), nil + if err := stringableRangeFromString(str, r); err != nil { + return err } - endStr, ok := monthsInv[r.End] - if !ok { - return nil, fmt.Errorf("unable to convert %d into month", r.End) + if r.Begin > r.End { + begin := monthsInv[r.Begin] + end := monthsInv[r.End] + return fmt.Errorf("end month %s is before start month %s", end, begin) } - rangeStr := fmt.Sprintf("%s:%s", beginStr, endStr) - return interface{}(rangeStr), nil + return nil } // UnmarshalYAML implements the Unmarshaller interface for YearRange. @@ -270,7 +241,7 @@ func (r *YearRange) UnmarshalYAML(unmarshal func(interface{}) error) error { } err := stringableRangeFromString(str, r) if r.Begin > r.End { - return errors.New("start day cannot be before end day") + return fmt.Errorf("end year %d is before start year %d", r.End, r.Begin) } return err } @@ -286,25 +257,36 @@ func (tr *TimeRange) UnmarshalYAML(unmarshal func(interface{}) error) error { } start, err := parseTime(y.StartTime) if err != nil { - return nil + return err } - End, err := parseTime(y.EndTime) + end, err := parseTime(y.EndTime) if err != nil { return err } - if start < 0 { - return errors.New("start time out of range") - } - if End > 1440 { - return errors.New("End time out of range") - } - if start >= End { + if start >= end { return errors.New("start time cannot be equal or greater than end time") } - tr.StartMinute, tr.EndMinute = start, End + tr.StartMinute, tr.EndMinute = start, end return nil } +// MarshalYAML implements the yaml.Marshaler interface for WeekdayRange +func (r WeekdayRange) MarshalYAML() (interface{}, error) { + beginStr, ok := daysOfWeekInv[r.Begin] + if !ok { + return nil, fmt.Errorf("unable to convert %d into weekday string", r.Begin) + } + if r.Begin == r.End { + return interface{}(beginStr), nil + } + endStr, ok := daysOfWeekInv[r.End] + if !ok { + return nil, fmt.Errorf("unable to convert %d into weekday string", r.End) + } + rangeStr := fmt.Sprintf("%s:%s", beginStr, endStr) + return interface{}(rangeStr), nil +} + //MarshalYAML implements the yaml.Marshaler interface for TimeRange func (tr TimeRange) MarshalYAML() (out interface{}, err error) { startHr := tr.StartMinute / 60 @@ -432,7 +414,7 @@ func (tp TimeInterval) ContainsTime(t time.Time) bool { return true } -// Converts a string of the form "HH:MM" into a TimeRange +// Converts a string of the form "HH:MM" into the number of minutes elapsed in the day func parseTime(in string) (mins int, err error) { if !validTimeRE.MatchString(in) { return 0, fmt.Errorf("couldn't parse timestamp %s, invalid format", in) diff --git a/timeinterval/timeinterval_test.go b/timeinterval/timeinterval_test.go index 0794b51c04..7c4652d119 100644 --- a/timeinterval/timeinterval_test.go +++ b/timeinterval/timeinterval_test.go @@ -181,6 +181,7 @@ var yamlUnmarshalTestCases = []struct { contains []string excludes []string expectError bool + err string }{ { // Simple business hours test @@ -262,12 +263,33 @@ var yamlUnmarshalTestCases = []struct { "01 Apr 21 13:00 GMT", }, }, + { + // Invalid start time + in: ` +--- +- times: + - start_time: '01:99' + end_time: '23:59'`, + expectError: true, + err: "couldn't parse timestamp 01:99, invalid format", + }, + { + // Invalid end time + in: ` +--- +- times: + - start_time: '00:00' + end_time: '99:99'`, + expectError: true, + err: "couldn't parse timestamp 99:99, invalid format", + }, { // Start day before End day in: ` --- - weekdays: ['friday:monday']`, expectError: true, + err: "start day cannot be before end day", }, { // Invalid weekdays @@ -276,6 +298,7 @@ var yamlUnmarshalTestCases = []struct { - weekdays: ['blurgsday:flurgsday'] `, expectError: true, + err: "blurgsday is not a valid weekday", }, { // 0 day of month @@ -284,14 +307,25 @@ var yamlUnmarshalTestCases = []struct { - days_of_month: ['0'] `, expectError: true, + err: "0 is not a valid day of the month: out of range", }, { - // Too early day of month + // Start day of month < 0 in: ` --- - days_of_month: ['-50:-20'] `, expectError: true, + err: "-50 is not a valid day of the month: out of range", + }, + { + // End day of month > 31 + in: ` +--- +- days_of_month: ['1:50'] +`, + expectError: true, + err: "50 is not a valid day of the month: out of range", }, { // Negative indices should work @@ -307,20 +341,58 @@ var yamlUnmarshalTestCases = []struct { expectError: false, }, { - // Negative start date before positive End date + // End day must be negative if begin day is negative in: ` --- - days_of_month: ['-15:5'] `, expectError: true, + err: "end day must be negative if start day is negative", }, { - // Negative End date before positive postive start date + // Negative end date before positive postive start date in: ` --- - days_of_month: ['10:-25'] `, expectError: true, + err: "end day -25 is always before start day 10", + }, + { + // Invalid start month + in: ` +--- +- months: ['martius:june'] +`, + expectError: true, + err: "martius is not a valid month", + }, + { + // Invalid end month + in: ` +--- +- months: ['march:junius'] +`, + expectError: true, + err: "junius is not a valid month", + }, + { + // Start month after end month + in: ` +--- +- months: ['december:january'] +`, + expectError: true, + err: "end month january is before start month december", + }, + { + // Start year after end year + in: ` +--- +- years: ['2022:2020'] +`, + expectError: true, + err: "end year 2020 is before start year 2022", }, } @@ -333,6 +405,9 @@ func TestYamlUnmarshal(t *testing.T) { } else if err == nil && tc.expectError { t.Errorf("Expected error when unmarshalling %s but didn't receive one", tc.in) } else if err != nil && tc.expectError { + if err.Error() != tc.err { + t.Errorf("Incorrect error: Want %s, got %s", tc.err, err.Error()) + } continue } if !reflect.DeepEqual(ti, tc.intervals) { From 2c86b696c96830e907815f0c1cd344356923440c Mon Sep 17 00:00:00 2001 From: Ben Ridley Date: Tue, 17 Nov 2020 11:26:29 +1100 Subject: [PATCH 24/47] Prevent clamping dates that start after the end of the month Signed-off-by: Ben Ridley --- timeinterval/timeinterval.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/timeinterval/timeinterval.go b/timeinterval/timeinterval.go index 0870785467..663d124be1 100644 --- a/timeinterval/timeinterval.go +++ b/timeinterval/timeinterval.go @@ -363,6 +363,10 @@ func (tp TimeInterval) ContainsTime(t time.Time) bool { } else { end = validDates.End } + // Skip clamping if the beginning date is after the end of the month + if begin > daysInMonth { + continue + } // Clamp to the boundaries of the month to prevent crossing into other months begin = clamp(begin, -1*daysInMonth, daysInMonth) end = clamp(end, -1*daysInMonth, daysInMonth) From 1957e3c80e2bf0a4e7e47414c31b8fadfd3419f0 Mon Sep 17 00:00:00 2001 From: Ben Ridley Date: Tue, 17 Nov 2020 11:26:46 +1100 Subject: [PATCH 25/47] Add some more complete test cases, test for broken clamping Signed-off-by: Ben Ridley --- timeinterval/timeinterval_test.go | 68 +++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/timeinterval/timeinterval_test.go b/timeinterval/timeinterval_test.go index 7c4652d119..5b0e299c62 100644 --- a/timeinterval/timeinterval_test.go +++ b/timeinterval/timeinterval_test.go @@ -506,3 +506,71 @@ func TestYamlMarshal(t *testing.T) { } } } + +var completeTestCases = []struct { + in string + contains []string + excludes []string +}{ + { + in: ` +--- +weekdays: ['monday:wednesday', 'saturday', 'sunday'] +times: + - start_time: '13:00' + end_time: '15:00' +days_of_month: ['1', '10', '20:-1'] +years: ['2020:2023'] +months: ['january:march'] +`, + contains: []string{ + "10 Jan 21 13:00 GMT", + "30 Jan 21 14:24 GMT", + }, + excludes: []string{ + "09 Jan 21 13:00 GMT", + "20 Jan 21 12:59 GMT", + "02 Feb 21 13:00 GMT", + }, + }, + { + // Check for broken clamping (clamping begin date after end of month to the end of the month) + in: ` +--- +days_of_month: ['30:31'] +years: ['2020:2023'] +months: ['february'] +`, + excludes: []string{ + "28 Feb 21 13:00 GMT", + }, + }, +} + +// Tests the entire flow from unmarshalling to containing a time +func TestTimeIntervalComplete(t *testing.T) { + for _, tc := range completeTestCases { + var ti TimeInterval + if err := yaml.Unmarshal([]byte(tc.in), &ti); err != nil { + t.Error(err) + } + for _, ts := range tc.contains { + tt, err := time.Parse(time.RFC822, ts) + if err != nil { + t.Error(err) + } + if !ti.ContainsTime(tt) { + t.Errorf("Expected %s to contain %s", tc.in, ts) + } + } + for _, ts := range tc.excludes { + tt, err := time.Parse(time.RFC822, ts) + if err != nil { + t.Error(err) + } + if ti.ContainsTime(tt) { + t.Errorf("Expected %s to exclude %s", tc.in, ts) + } + } + } +} From 2c5c17a16a9807d6610d4ea080aa0590a914e135 Mon Sep 17 00:00:00 2001 From: Ben Ridley Date: Tue, 24 Nov 2020 08:36:42 +1100 Subject: [PATCH 26/47] Update timeinterval/timeinterval.go Co-authored-by: Julien Pivotto Signed-off-by: Ben Ridley --- timeinterval/timeinterval.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/timeinterval/timeinterval.go b/timeinterval/timeinterval.go index 663d124be1..4a8d8f33ac 100644 --- a/timeinterval/timeinterval.go +++ b/timeinterval/timeinterval.go @@ -32,7 +32,7 @@ type TimeInterval struct { Years []YearRange `yaml:"years,flow,omitempty"` } -/* TimeRange represents a range of minutes within a 1440 minute day, exclusive of the End minute. A day consists of 1440 minutes. +// TimeRange represents a range of minutes within a 1440 minute day, exclusive of the End minute. A day consists of 1440 minutes. For example, 5:00PM to End of the day would Begin at 1020 and End at 1440. */ type TimeRange struct { StartMinute int From 70b138a17a04017cc8709ffd18c3d278c052343b Mon Sep 17 00:00:00 2001 From: Ben Ridley Date: Tue, 24 Nov 2020 12:42:27 +1100 Subject: [PATCH 27/47] Apply formatting suggestions from code review Co-authored-by: Julien Pivotto Signed-off-by: Ben Ridley --- config/config.go | 2 +- docs/configuration.md | 1 - notify/notify.go | 5 +++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/config/config.go b/config/config.go index 128bff5cad..c7b341ed33 100644 --- a/config/config.go +++ b/config/config.go @@ -220,7 +220,7 @@ func resolveFilepaths(baseDir string, cfg *Config) { } } -// A MuteTimeInterval represents a named set of time intervals for which a route should be muted. +// MuteTimeInterval represents a named set of time intervals for which a route should be muted. type MuteTimeInterval struct { Name string `yaml:"name" json:"name"` TimeIntervals []timeinterval.TimeInterval `yaml:"time_intervals"` diff --git a/docs/configuration.md b/docs/configuration.md index 96413c3db0..20757137d9 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -222,7 +222,6 @@ name: time_intervals: [ - ... ] -``` ## `` A `time_interval` contains the actual definition for an interval of time. The syntax supports the following fields: diff --git a/notify/notify.go b/notify/notify.go index dd776819a9..0ce01b744b 100644 --- a/notify/notify.go +++ b/notify/notify.go @@ -312,7 +312,7 @@ func (pb *PipelineBuilder) New( ms := NewGossipSettleStage(peer) is := NewMuteStage(inhibitor) ss := NewMuteStage(silencer) - mts := NewTimeMuteStage(muteTimes) + tms := NewTimeMuteStage(muteTimes) for name := range receivers { st := createReceiverStage(name, receivers[name], wait, notificationLog, pb.metrics) @@ -782,7 +782,7 @@ func NewTimeMuteStage(mt map[string][]timeinterval.TimeInterval) *TimeMuteStage // Exec implements the stage interface for TimeMuteStage // TimeMuteStage is responsible for muting alerts whose route is not in an active time -func (mts TimeMuteStage) Exec(ctx context.Context, l log.Logger, alerts ...*types.Alert) (context.Context, []*types.Alert, error) { +func (tms TimeMuteStage) Exec(ctx context.Context, l log.Logger, alerts ...*types.Alert) (context.Context, []*types.Alert, error) { muteTimeNames, ok := MuteTimeNames(ctx) if !ok { return ctx, alerts, nil @@ -801,6 +801,7 @@ func (mts TimeMuteStage) Exec(ctx context.Context, l log.Logger, alerts ...*type for _, ti := range mt { if ti.ContainsTime(now) { muted = true + break Loop } } } From fb60329aaddfb8514eb8ade56a403c3c9c464272 Mon Sep 17 00:00:00 2001 From: Ben Ridley Date: Tue, 24 Nov 2020 12:45:40 +1100 Subject: [PATCH 28/47] Add fullstops to comments Signed-off-by: Ben Ridley --- notify/notify.go | 11 ++++++----- timeinterval/timeinterval.go | 34 +++++++++++++++++----------------- 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/notify/notify.go b/notify/notify.go index 0ce01b744b..a19dbb95eb 100644 --- a/notify/notify.go +++ b/notify/notify.go @@ -316,7 +316,7 @@ func (pb *PipelineBuilder) New( for name := range receivers { st := createReceiverStage(name, receivers[name], wait, notificationLog, pb.metrics) - rs[name] = MultiStage{ms, is, mts, ss, st} + rs[name] = MultiStage{ms, is, tms, ss, st} } return rs } @@ -780,8 +780,8 @@ func NewTimeMuteStage(mt map[string][]timeinterval.TimeInterval) *TimeMuteStage return &TimeMuteStage{mt} } -// Exec implements the stage interface for TimeMuteStage -// TimeMuteStage is responsible for muting alerts whose route is not in an active time +// Exec implements the stage interface for TimeMuteStage. +// TimeMuteStage is responsible for muting alerts whose route is not in an active time. func (tms TimeMuteStage) Exec(ctx context.Context, l log.Logger, alerts ...*types.Alert) (context.Context, []*types.Alert, error) { muteTimeNames, ok := MuteTimeNames(ctx) if !ok { @@ -793,8 +793,9 @@ func (tms TimeMuteStage) Exec(ctx context.Context, l log.Logger, alerts ...*type } muted := false +Loop: for _, mtName := range muteTimeNames { - mt, ok := mts.muteTimes[mtName] + mt, ok := tms.muteTimes[mtName] if !ok { return ctx, alerts, errors.Errorf("mute time %s doesn't exist in config", mtName) } @@ -805,7 +806,7 @@ func (tms TimeMuteStage) Exec(ctx context.Context, l log.Logger, alerts ...*type } } } - // If the current time is inside a mute time, all alerts are removed from the pipeline + // If the current time is inside a mute time, all alerts are removed from the pipeline. if muted { lvl := level.Debug(l) lvl.Log("msg", "Notifications not sent, route is within mute time") diff --git a/timeinterval/timeinterval.go b/timeinterval/timeinterval.go index 4a8d8f33ac..af0ca08bfd 100644 --- a/timeinterval/timeinterval.go +++ b/timeinterval/timeinterval.go @@ -33,34 +33,34 @@ type TimeInterval struct { } // TimeRange represents a range of minutes within a 1440 minute day, exclusive of the End minute. A day consists of 1440 minutes. - For example, 5:00PM to End of the day would Begin at 1020 and End at 1440. */ +// For example, 4:00PM to End of the day would Begin at 1020 and End at 1440. */ type TimeRange struct { StartMinute int EndMinute int } -// InclusiveRange is used to hold the Beginning and End values of many time interval components +// InclusiveRange is used to hold the Beginning and End values of many time interval components. type InclusiveRange struct { Begin int End int } -// A WeekdayRange is an inclusive range between [0, 6] where 0 = Sunday +// A WeekdayRange is an inclusive range between [0, 6] where 0 = Sunday. type WeekdayRange struct { InclusiveRange } -// A DayOfMonthRange is an inclusive range that may have negative Beginning/End values that represent distance from the End of the month Beginning at -1 +// A DayOfMonthRange is an inclusive range that may have negative Beginning/End values that represent distance from the End of the month Beginning at -1. type DayOfMonthRange struct { InclusiveRange } -// A MonthRange is an inclusive range between [1, 12] where 1 = January +// A MonthRange is an inclusive range between [1, 12] where 1 = January. type MonthRange struct { InclusiveRange } -// A YearRange is a positive inclusive range +// A YearRange is a positive inclusive range. type YearRange struct { InclusiveRange } @@ -70,7 +70,7 @@ type yamlTimeRange struct { EndTime string `yaml:"end_time"` } -// A range with a Beginning and End that can be represented as strings +// A range with a Beginning and End that can be represented as strings. type stringableRange interface { setBegin(int) setEnd(int) @@ -201,7 +201,7 @@ func (r *DayOfMonthRange) UnmarshalYAML(unmarshal func(interface{}) error) error return fmt.Errorf("end day must be negative if start day is negative") } // Check begin <= end. We can't know this for sure when using negative indices - // but we can prevent cases where its always invalid (using 28 day minimum length) + // but we can prevent cases where its always invalid (using 28 day minimum length). checkBegin := r.Begin checkEnd := r.End if r.Begin < 0 { @@ -270,7 +270,7 @@ func (tr *TimeRange) UnmarshalYAML(unmarshal func(interface{}) error) error { return nil } -// MarshalYAML implements the yaml.Marshaler interface for WeekdayRange +// MarshalYAML implements the yaml.Marshaler interface for WeekdayRange. func (r WeekdayRange) MarshalYAML() (interface{}, error) { beginStr, ok := daysOfWeekInv[r.Begin] if !ok { @@ -287,7 +287,7 @@ func (r WeekdayRange) MarshalYAML() (interface{}, error) { return interface{}(rangeStr), nil } -//MarshalYAML implements the yaml.Marshaler interface for TimeRange +//MarshalYAML implements the yaml.Marshaler interface for TimeRange. func (tr TimeRange) MarshalYAML() (out interface{}, err error) { startHr := tr.StartMinute / 60 endHr := tr.EndMinute / 60 @@ -301,7 +301,7 @@ func (tr TimeRange) MarshalYAML() (out interface{}, err error) { return interface{}(yTr), err } -//MarshalYAML implements the yaml.Marshaler interface for InclusiveRange +//MarshalYAML implements the yaml.Marshaler interface for InclusiveRange. func (ir InclusiveRange) MarshalYAML() (interface{}, error) { if ir.Begin == ir.End { return strconv.Itoa(ir.Begin), nil @@ -310,7 +310,7 @@ func (ir InclusiveRange) MarshalYAML() (interface{}, error) { return interface{}(out), nil } -// TimeLayout specifies the layout to be used in time.Parse() calls for time intervals +// TimeLayout specifies the layout to be used in time.Parse() calls for time intervals. const TimeLayout = "15:04" var validTime string = "^((([01][0-9])|(2[0-3])):[0-5][0-9])$|(^24:00$)" @@ -334,7 +334,7 @@ func clamp(n, min, max int) int { return n } -// ContainsTime returns true if the TimeInterval contains the given time, otherwise returns false +// ContainsTime returns true if the TimeInterval contains the given time, otherwise returns false. func (tp TimeInterval) ContainsTime(t time.Time) bool { if tp.Times != nil { in := false @@ -363,11 +363,11 @@ func (tp TimeInterval) ContainsTime(t time.Time) bool { } else { end = validDates.End } - // Skip clamping if the beginning date is after the end of the month + // Skip clamping if the beginning date is after the end of the month. if begin > daysInMonth { continue } - // Clamp to the boundaries of the month to prevent crossing into other months + // Clamp to the boundaries of the month to prevent crossing into other months. begin = clamp(begin, -1*daysInMonth, daysInMonth) end = clamp(end, -1*daysInMonth, daysInMonth) if t.Day() >= begin && t.Day() <= end { @@ -418,7 +418,7 @@ func (tp TimeInterval) ContainsTime(t time.Time) bool { return true } -// Converts a string of the form "HH:MM" into the number of minutes elapsed in the day +// Converts a string of the form "HH:MM" into the number of minutes elapsed in the day. func parseTime(in string) (mins int, err error) { if !validTimeRE.MatchString(in) { return 0, fmt.Errorf("couldn't parse timestamp %s, invalid format", in) @@ -443,7 +443,7 @@ func parseTime(in string) (mins int, err error) { return mins, nil } -// Converts a range that can be represented as strings (e.g. monday:wednesday) into an equivalent integer-represented range +// Converts a range that can be represented as strings (e.g. monday:wednesday) into an equivalent integer-represented range. func stringableRangeFromString(in string, r stringableRange) (err error) { in = strings.ToLower(in) if strings.ContainsRune(in, ':') { From 11b643dd23b89d8f23b6bc185ce311963ac6ba15 Mon Sep 17 00:00:00 2001 From: Ben Ridley Date: Tue, 24 Nov 2020 13:57:32 +1100 Subject: [PATCH 29/47] Remove superfluous test case Signed-off-by: Ben Ridley --- timeinterval/timeinterval_test.go | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/timeinterval/timeinterval_test.go b/timeinterval/timeinterval_test.go index 5b0e299c62..9365c4e14a 100644 --- a/timeinterval/timeinterval_test.go +++ b/timeinterval/timeinterval_test.go @@ -163,18 +163,6 @@ var timeStringTestCases = []struct { }, } -var dayOfWeekStringTestCases = []struct { - dowString string - ranges []WeekdayRange - expectError bool -}{ - { - dowString: "['monday:friday', 'saturday']", - ranges: []WeekdayRange{{InclusiveRange{Begin: 1, End: 5}}, {InclusiveRange{Begin: 6, End: 6}}}, - expectError: false, - }, -} - var yamlUnmarshalTestCases = []struct { in string intervals []TimeInterval @@ -471,20 +459,6 @@ func TestParseTimeString(t *testing.T) { } } -func TestParseWeek(t *testing.T) { - for _, tc := range dayOfWeekStringTestCases { - var wr []WeekdayRange - err := yaml.Unmarshal([]byte(tc.dowString), &wr) - if err != nil && !tc.expectError { - t.Errorf("Received unexpected error: %v when parsing %v", err, tc.dowString) - } else if err == nil && tc.expectError { - t.Errorf("Expected error for invalid string %s but didn't receive one", tc.dowString) - } else if !reflect.DeepEqual(wr, tc.ranges) { - t.Errorf("Error parsing time string %s: Want %+v, got %+v", tc.dowString, tc.ranges, wr) - } - } -} - func TestYamlMarshal(t *testing.T) { for _, tc := range yamlUnmarshalTestCases { if tc.expectError { From 1cc736c4c8393f0e5e870f1ecc28852df6b44fe3 Mon Sep 17 00:00:00 2001 From: Ben Ridley Date: Tue, 24 Nov 2020 14:12:13 +1100 Subject: [PATCH 30/47] Return from errors earlier Signed-off-by: Ben Ridley --- timeinterval/timeinterval.go | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/timeinterval/timeinterval.go b/timeinterval/timeinterval.go index af0ca08bfd..ae7a1e9e4d 100644 --- a/timeinterval/timeinterval.go +++ b/timeinterval/timeinterval.go @@ -168,7 +168,9 @@ func (r *WeekdayRange) UnmarshalYAML(unmarshal func(interface{}) error) error { if err := unmarshal(&str); err != nil { return err } - err := stringableRangeFromString(str, r) + if err := stringableRangeFromString(str, r); err != nil { + return err + } if r.Begin > r.End { return errors.New("start day cannot be before end day") } @@ -178,7 +180,7 @@ func (r *WeekdayRange) UnmarshalYAML(unmarshal func(interface{}) error) error { if r.End < 0 || r.End > 6 { return fmt.Errorf("%s is not a valid day of the week: out of range", str) } - return err + return nil } // UnmarshalYAML implements the Unmarshaller interface for DayOfMonthRange. @@ -187,7 +189,9 @@ func (r *DayOfMonthRange) UnmarshalYAML(unmarshal func(interface{}) error) error if err := unmarshal(&str); err != nil { return err } - err := stringableRangeFromString(str, r) + if err := stringableRangeFromString(str, r); err != nil { + return err + } // Check beginning <= end accounting for negatives day of month indices as well. // Months != 31 days can't be addressed here and are clamped, but at least we can catch blatant errors. if r.Begin == 0 || r.Begin < -31 || r.Begin > 31 { @@ -213,7 +217,7 @@ func (r *DayOfMonthRange) UnmarshalYAML(unmarshal func(interface{}) error) error if checkBegin > checkEnd { return fmt.Errorf("end day %d is always before start day %d", r.End, r.Begin) } - return err + return nil } // UnmarshalYAML implements the Unmarshaller interface for MonthRange. @@ -239,11 +243,13 @@ func (r *YearRange) UnmarshalYAML(unmarshal func(interface{}) error) error { if err := unmarshal(&str); err != nil { return err } - err := stringableRangeFromString(str, r) + if err := stringableRangeFromString(str, r); err != nil { + return err + } if r.Begin > r.End { return fmt.Errorf("end year %d is before start year %d", r.End, r.Begin) } - return err + return nil } // UnmarshalYAML implements the Unmarshaller interface for TimeRanges. From 8e03268af9618255243f865a9155ee6f275d7299 Mon Sep 17 00:00:00 2001 From: Ben Ridley Date: Tue, 24 Nov 2020 14:12:28 +1100 Subject: [PATCH 31/47] Add additional test cases from code review Signed-off-by: Ben Ridley --- timeinterval/timeinterval_test.go | 59 +++++++++++++++++++++++-------- 1 file changed, 45 insertions(+), 14 deletions(-) diff --git a/timeinterval/timeinterval_test.go b/timeinterval/timeinterval_test.go index 9365c4e14a..6ca932de63 100644 --- a/timeinterval/timeinterval_test.go +++ b/timeinterval/timeinterval_test.go @@ -252,7 +252,7 @@ var yamlUnmarshalTestCases = []struct { }, }, { - // Invalid start time + // Invalid start time. in: ` --- - times: @@ -262,7 +262,7 @@ var yamlUnmarshalTestCases = []struct { err: "couldn't parse timestamp 01:99, invalid format", }, { - // Invalid end time + // Invalid end time. in: ` --- - times: @@ -272,7 +272,7 @@ var yamlUnmarshalTestCases = []struct { err: "couldn't parse timestamp 99:99, invalid format", }, { - // Start day before End day + // Start day before end day. in: ` --- - weekdays: ['friday:monday']`, @@ -280,7 +280,7 @@ var yamlUnmarshalTestCases = []struct { err: "start day cannot be before end day", }, { - // Invalid weekdays + // Invalid weekdays. in: ` --- - weekdays: ['blurgsday:flurgsday'] @@ -289,7 +289,25 @@ var yamlUnmarshalTestCases = []struct { err: "blurgsday is not a valid weekday", }, { - // 0 day of month + // Numeric weekdays aren't allowed. + in: ` +--- +- weekdays: ['1:3'] +`, + expectError: true, + err: "1 is not a valid weekday", + }, + { + // Negative numeric weekdays aren't allowed. + in: ` +--- +- weekdays: ['-2:-1'] +`, + expectError: true, + err: "-2 is not a valid weekday", + }, + { + // 0 day of month. in: ` --- - days_of_month: ['0'] @@ -298,7 +316,7 @@ var yamlUnmarshalTestCases = []struct { err: "0 is not a valid day of the month: out of range", }, { - // Start day of month < 0 + // Start day of month < 0. in: ` --- - days_of_month: ['-50:-20'] @@ -307,7 +325,7 @@ var yamlUnmarshalTestCases = []struct { err: "-50 is not a valid day of the month: out of range", }, { - // End day of month > 31 + // End day of month > 31. in: ` --- - days_of_month: ['1:50'] @@ -316,7 +334,7 @@ var yamlUnmarshalTestCases = []struct { err: "50 is not a valid day of the month: out of range", }, { - // Negative indices should work + // Negative indices should work. in: ` --- - days_of_month: ['1:-1'] @@ -329,7 +347,7 @@ var yamlUnmarshalTestCases = []struct { expectError: false, }, { - // End day must be negative if begin day is negative + // End day must be negative if begin day is negative. in: ` --- - days_of_month: ['-15:5'] @@ -338,7 +356,7 @@ var yamlUnmarshalTestCases = []struct { err: "end day must be negative if start day is negative", }, { - // Negative end date before positive postive start date + // Negative end date before positive postive start date. in: ` --- - days_of_month: ['10:-25'] @@ -347,7 +365,20 @@ var yamlUnmarshalTestCases = []struct { err: "end day -25 is always before start day 10", }, { - // Invalid start month + // Months should work regardless of case + in: ` +--- +- months: ['January:december'] +`, + expectError: false, + intervals: []TimeInterval{ + { + Months: []MonthRange{{InclusiveRange{1, 12}}}, + }, + }, + }, + { + // Invalid start month. in: ` --- - months: ['martius:june'] @@ -356,7 +387,7 @@ var yamlUnmarshalTestCases = []struct { err: "martius is not a valid month", }, { - // Invalid end month + // Invalid end month. in: ` --- - months: ['march:junius'] @@ -365,7 +396,7 @@ var yamlUnmarshalTestCases = []struct { err: "junius is not a valid month", }, { - // Start month after end month + // Start month after end month. in: ` --- - months: ['december:january'] @@ -374,7 +405,7 @@ var yamlUnmarshalTestCases = []struct { err: "end month january is before start month december", }, { - // Start year after end year + // Start year after end year. in: ` --- - years: ['2022:2020'] From fa2fab64ded714519e649c1c5645d23a130d6881 Mon Sep 17 00:00:00 2001 From: Ben Ridley Date: Tue, 24 Nov 2020 14:14:11 +1100 Subject: [PATCH 32/47] Simplify logging on time mute Signed-off-by: Ben Ridley --- notify/notify.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/notify/notify.go b/notify/notify.go index a19dbb95eb..4f85060042 100644 --- a/notify/notify.go +++ b/notify/notify.go @@ -808,8 +808,7 @@ Loop: } // If the current time is inside a mute time, all alerts are removed from the pipeline. if muted { - lvl := level.Debug(l) - lvl.Log("msg", "Notifications not sent, route is within mute time") + level.Debug(l).Log("msg", "Notifications not sent, route is within mute time") return ctx, nil, nil } return ctx, alerts, nil From c34003ffdb321e2e3b2db12bf444597034df2d7c Mon Sep 17 00:00:00 2001 From: Ben Ridley Date: Tue, 24 Nov 2020 14:16:26 +1100 Subject: [PATCH 33/47] Pre-allocate mute time config slice Signed-off-by: Ben Ridley --- cmd/alertmanager/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/alertmanager/main.go b/cmd/alertmanager/main.go index 7e8aaba532..345e04c935 100644 --- a/cmd/alertmanager/main.go +++ b/cmd/alertmanager/main.go @@ -415,7 +415,7 @@ func run() int { } // Build the map of time interval names to mute time definitions - muteTimes := make(map[string][]timeinterval.TimeInterval) + muteTimes := make(map[string][]timeinterval.TimeInterval, len(conf.MuteTimeIntervals)) for _, ti := range conf.MuteTimeIntervals { muteTimes[ti.Name] = ti.TimeIntervals } From ae116cfc26c00b1edd650885ccd3a4923a3c43e6 Mon Sep 17 00:00:00 2001 From: Ben Ridley Date: Tue, 24 Nov 2020 14:38:12 +1100 Subject: [PATCH 34/47] Fix comment formatting Signed-off-by: Ben Ridley --- cmd/alertmanager/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/alertmanager/main.go b/cmd/alertmanager/main.go index 345e04c935..641029603b 100644 --- a/cmd/alertmanager/main.go +++ b/cmd/alertmanager/main.go @@ -414,7 +414,7 @@ func run() int { integrationsNum += len(integrations) } - // Build the map of time interval names to mute time definitions + // Build the map of time interval names to mute time definitions. muteTimes := make(map[string][]timeinterval.TimeInterval, len(conf.MuteTimeIntervals)) for _, ti := range conf.MuteTimeIntervals { muteTimes[ti.Name] = ti.TimeIntervals From 5152a2fbbadef397d7b9be188c62417a11977a14 Mon Sep 17 00:00:00 2001 From: Ben Ridley Date: Tue, 24 Nov 2020 14:38:32 +1100 Subject: [PATCH 35/47] Ensure mute time intervals are unique in config, add associated test Signed-off-by: Ben Ridley --- config/config.go | 3 +++ config/config_test.go | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/config/config.go b/config/config.go index c7b341ed33..052d5db97b 100644 --- a/config/config.go +++ b/config/config.go @@ -442,6 +442,9 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error { tiNames := make(map[string]struct{}) for _, mt := range c.MuteTimeIntervals { + if _, ok := tiNames[mt.Name]; ok { + return fmt.Errorf("mute time interval %q is not unique", mt.Name) + } tiNames[mt.Name] = struct{}{} } return checkTimeInterval(c.Route, tiNames) diff --git a/config/config_test.go b/config/config_test.go index fd4afae17a..314993b6b2 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -210,6 +210,44 @@ route: } +func TestMuteTimeNoDuplicates(t *testing.T) { + in := ` +mute_time_intervals: +- name: duplicate + time_intervals: + - times: + - start_time: '09:00' + end_time: '17:00' +- name: duplicate + time_intervals: + - times: + - start_time: '10:00' + end_time: '14:00' + +receivers: +- name: 'team-X-mails' + +route: + receiver: 'team-X-mails' + routes: + - match: + severity: critical + mute_times: + - business_hours +` + _, err := Load(in) + + expected := "mute time interval \"duplicate\" is not unique" + + if err == nil { + t.Fatalf("no error returned, expected:\n%q", expected) + } + if err.Error() != expected { + t.Errorf("\nexpected:\n%q\ngot:\n%q", expected, err.Error()) + } + +} + func TestGroupByHasNoDuplicatedLabels(t *testing.T) { in := ` route: From 5d4231b0016f52aa56c6cde6d661e1ef1e301447 Mon Sep 17 00:00:00 2001 From: Ben Ridley Date: Tue, 24 Nov 2020 15:02:07 +1100 Subject: [PATCH 36/47] Use consistent naming for mute time intervals Signed-off-by: Ben Ridley --- config/config.go | 8 ++++---- config/config_test.go | 8 ++++---- dispatch/dispatch.go | 2 +- dispatch/route.go | 18 +++++++++--------- notify/notify.go | 18 +++++++++--------- notify/notify_test.go | 2 +- 6 files changed, 28 insertions(+), 28 deletions(-) diff --git a/config/config.go b/config/config.go index 052d5db97b..15de3158b8 100644 --- a/config/config.go +++ b/config/config.go @@ -431,7 +431,7 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error { if len(c.Route.Match) > 0 || len(c.Route.MatchRE) > 0 { return fmt.Errorf("root route must not have any matchers") } - if len(c.Route.MuteTimes) > 0 { + if len(c.Route.MuteTimeIntervals) > 0 { return fmt.Errorf("root route cannot have any mute times") } @@ -473,10 +473,10 @@ func checkTimeInterval(r *Route, timeIntervals map[string]struct{}) error { return err } } - if len(r.MuteTimes) == 0 { + if len(r.MuteTimeIntervals) == 0 { return nil } - for _, mt := range r.MuteTimes { + for _, mt := range r.MuteTimeIntervals { if _, ok := timeIntervals[mt]; !ok { return fmt.Errorf("undefined time interval %q used in route", mt) } @@ -632,7 +632,7 @@ type Route struct { Match map[string]string `yaml:"match,omitempty" json:"match,omitempty"` MatchRE MatchRegexps `yaml:"match_re,omitempty" json:"match_re,omitempty"` - MuteTimes []string `yaml:"mute_times,omitempty" json:"mute_times,omitempty"` + MuteTimeIntervals []string `yaml:"mute_time_intervals,omitempty" json:"mute_time_intervals,omitempty"` Continue bool `yaml:"continue" json:"continue,omitempty"` Routes []*Route `yaml:"routes,omitempty" json:"routes,omitempty"` diff --git a/config/config_test.go b/config/config_test.go index 314993b6b2..0fe587cd29 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -158,7 +158,7 @@ route: routes: - match: severity: critical - mute_times: + mute_time_intervals: - business_hours receivers: @@ -194,7 +194,7 @@ route: routes: - match: severity: critical - mute_times: + mute_time_intervals: - business_hours ` _, err := Load(in) @@ -232,7 +232,7 @@ route: routes: - match: severity: critical - mute_times: + mute_time_intervals: - business_hours ` _, err := Load(in) @@ -342,7 +342,7 @@ receivers: route: receiver: 'team-X-mails' - mute_times: + mute_time_intervals: - my_mute_time ` _, err := Load(in) diff --git a/dispatch/dispatch.go b/dispatch/dispatch.go index df55f9dfef..b030046d6d 100644 --- a/dispatch/dispatch.go +++ b/dispatch/dispatch.go @@ -404,7 +404,7 @@ func (ag *aggrGroup) run(nf notifyFunc) { ctx = notify.WithGroupLabels(ctx, ag.labels) ctx = notify.WithReceiverName(ctx, ag.opts.Receiver) ctx = notify.WithRepeatInterval(ctx, ag.opts.RepeatInterval) - ctx = notify.WithMuteTimes(ctx, ag.opts.MuteTimes) + ctx = notify.WithMuteTimeIntervals(ctx, ag.opts.MuteTimeIntervals) // Wait the configured interval before calling flush again. ag.mtx.Lock() diff --git a/dispatch/route.go b/dispatch/route.go index bcb3f5aec7..8708248859 100644 --- a/dispatch/route.go +++ b/dispatch/route.go @@ -29,12 +29,12 @@ import ( // DefaultRouteOpts are the defaulting routing options which apply // to the root route of a routing tree. var DefaultRouteOpts = RouteOpts{ - GroupWait: 30 * time.Second, - GroupInterval: 5 * time.Minute, - RepeatInterval: 4 * time.Hour, - GroupBy: map[model.LabelName]struct{}{}, - GroupByAll: false, - MuteTimes: []string{}, + GroupWait: 30 * time.Second, + GroupInterval: 5 * time.Minute, + RepeatInterval: 4 * time.Hour, + GroupBy: map[model.LabelName]struct{}{}, + GroupByAll: false, + MuteTimeIntervals: []string{}, } // A Route is a node that contains definitions of how to handle alerts. @@ -117,7 +117,7 @@ func NewRoute(cr *config.Route, parent *Route) *Route { sort.Sort(matchers) - opts.MuteTimes = cr.MuteTimes + opts.MuteTimeIntervals = cr.MuteTimeIntervals route := &Route{ parent: parent, @@ -208,8 +208,8 @@ type RouteOpts struct { GroupInterval time.Duration RepeatInterval time.Duration - // A list of time intervals for which the route is muted - MuteTimes []string + // A list of time intervals for which the route is muted. + MuteTimeIntervals []string } func (ro *RouteOpts) String() string { diff --git a/notify/notify.go b/notify/notify.go index 4f85060042..3a75886760 100644 --- a/notify/notify.go +++ b/notify/notify.go @@ -109,7 +109,7 @@ const ( keyFiringAlerts keyResolvedAlerts keyNow - keyMuteTimes + keyMuteTimeIntervals ) // WithReceiverName populates a context with a receiver name. @@ -147,9 +147,9 @@ func WithRepeatInterval(ctx context.Context, t time.Duration) context.Context { return context.WithValue(ctx, keyRepeatInterval, t) } -// WithMuteTimes populates a context with a slice of mute time names. -func WithMuteTimes(ctx context.Context, mt []string) context.Context { - return context.WithValue(ctx, keyMuteTimes, mt) +// WithMuteTimeIntervals populates a context with a slice of mute time names. +func WithMuteTimeIntervals(ctx context.Context, mt []string) context.Context { + return context.WithValue(ctx, keyMuteTimeIntervals, mt) } // RepeatInterval extracts a repeat interval from the context. Iff none exists, the @@ -201,10 +201,10 @@ func ResolvedAlerts(ctx context.Context) ([]uint64, bool) { return v, ok } -// MuteTimeNames extracts a slice of mute time names from the context. Iff none exists, the +// MuteTimeIntervalNames extracts a slice of mute time names from the context. Iff none exists, the // second argument is false. -func MuteTimeNames(ctx context.Context) ([]string, bool) { - v, ok := ctx.Value(keyMuteTimes).([]string) +func MuteTimeIntervalNames(ctx context.Context) ([]string, bool) { + v, ok := ctx.Value(keyMuteTimeIntervals).([]string) return v, ok } @@ -783,7 +783,7 @@ func NewTimeMuteStage(mt map[string][]timeinterval.TimeInterval) *TimeMuteStage // Exec implements the stage interface for TimeMuteStage. // TimeMuteStage is responsible for muting alerts whose route is not in an active time. func (tms TimeMuteStage) Exec(ctx context.Context, l log.Logger, alerts ...*types.Alert) (context.Context, []*types.Alert, error) { - muteTimeNames, ok := MuteTimeNames(ctx) + muteTimeIntervalNames, ok := MuteTimeIntervalNames(ctx) if !ok { return ctx, alerts, nil } @@ -794,7 +794,7 @@ func (tms TimeMuteStage) Exec(ctx context.Context, l log.Logger, alerts ...*type muted := false Loop: - for _, mtName := range muteTimeNames { + for _, mtName := range muteTimeIntervalNames { mt, ok := tms.muteTimes[mtName] if !ok { return ctx, alerts, errors.Errorf("mute time %s doesn't exist in config", mtName) diff --git a/notify/notify_test.go b/notify/notify_test.go index cc6cfd2e7a..660c37095c 100644 --- a/notify/notify_test.go +++ b/notify/notify_test.go @@ -787,7 +787,7 @@ func TestTimeMuteStage(t *testing.T) { alerts := []*types.Alert{{Alert: a}} ctx := context.Background() ctx = WithNow(ctx, now) - ctx = WithMuteTimes(ctx, []string{"test"}) + ctx = WithMuteTimeIntervals(ctx, []string{"test"}) _, out, err := stage.Exec(ctx, log.NewNopLogger(), alerts...) if err != nil { From 24804e6b09f1915dc5b872178c45b66956cd0529 Mon Sep 17 00:00:00 2001 From: Ben Ridley Date: Tue, 24 Nov 2020 15:05:37 +1100 Subject: [PATCH 37/47] Improve comment fomatting Signed-off-by: Ben Ridley --- timeinterval/timeinterval.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/timeinterval/timeinterval.go b/timeinterval/timeinterval.go index ae7a1e9e4d..e62fb56037 100644 --- a/timeinterval/timeinterval.go +++ b/timeinterval/timeinterval.go @@ -33,7 +33,7 @@ type TimeInterval struct { } // TimeRange represents a range of minutes within a 1440 minute day, exclusive of the End minute. A day consists of 1440 minutes. -// For example, 4:00PM to End of the day would Begin at 1020 and End at 1440. */ +// For example, 4:00PM to End of the day would Begin at 1020 and End at 1440. type TimeRange struct { StartMinute int EndMinute int @@ -444,7 +444,7 @@ func parseTime(in string) (mins int, err error) { if timeStampHours < 0 || timeStampHours > 24 || timeStampMinutes < 0 || timeStampMinutes > 60 { return 0, fmt.Errorf("timestamp %s out of range", in) } - // Timestamps are stored as minutes elapsed in the day, so multiply hours by 60 + // Timestamps are stored as minutes elapsed in the day, so multiply hours by 60. mins = timeStampHours*60 + timeStampMinutes return mins, nil } From 7ffd4ca4f4217057a745649ce0d7ef3d29c659dc Mon Sep 17 00:00:00 2001 From: Ben Ridley Date: Tue, 24 Nov 2020 15:05:58 +1100 Subject: [PATCH 38/47] Change docs to reflect mute_time_intervals in routes instead of mute_times Signed-off-by: Ben Ridley --- docs/configuration.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 20757137d9..4659aefb64 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -114,7 +114,7 @@ receivers: inhibit_rules: [ - ... ] -# A list of mute time intervals for muting routes +# A list of mute time intervals for muting routes. mute_time_intervals: [ - ... ] ``` @@ -172,10 +172,10 @@ match_re: # been sent successfully for an alert. (Usually ~3h or more). [ repeat_interval: | default = 4h ] -# Times when the route should be muted. These must match a name of a +# Times when the route should be muted. These must match the name of a # mute time interval defined in the mute_time_intervals section. # Additionally, the root node cannot have any mute times. -mute_times: +mute_time_intervals: [ - ...] # Zero or more child routes. @@ -221,7 +221,7 @@ in the routing tree to mute particular routes for particular times of the day. name: time_intervals: [ - ... ] - +``` ## `` A `time_interval` contains the actual definition for an interval of time. The syntax supports the following fields: From 0fe51de6e3cfb59dbb86b8e27004b3092a04c39f Mon Sep 17 00:00:00 2001 From: Ben Ridley Date: Tue, 24 Nov 2020 15:15:58 +1100 Subject: [PATCH 39/47] Clarify timezone support in mute time intervals Signed-off-by: Ben Ridley --- docs/configuration.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index 4659aefb64..046affc456 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -240,7 +240,8 @@ supports the following fields: ``` All these fields are optional and if left unspecified allow any value to match the interval. -Some fields support ranges and negative indices, and are detailed below: +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. `times`: A list of time-ranges. They are inclusive of the starting time and exclusive of the ending time to make it easy to represent times that start/end on hour boundaries. From d1774718226f4a1699cfce083bc71ef74b105360 Mon Sep 17 00:00:00 2001 From: Ben Ridley Date: Tue, 24 Nov 2020 15:34:13 +1100 Subject: [PATCH 40/47] Improve comment formatting Signed-off-by: Ben Ridley --- config/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.go b/config/config.go index 15de3158b8..2d7a780b62 100644 --- a/config/config.go +++ b/config/config.go @@ -226,7 +226,7 @@ type MuteTimeInterval struct { TimeIntervals []timeinterval.TimeInterval `yaml:"time_intervals"` } -// UnmarshalYAML implements the yaml.Unmarshaler interface for MuteTimeInterval +// UnmarshalYAML implements the yaml.Unmarshaler interface for MuteTimeInterval. func (mt *MuteTimeInterval) UnmarshalYAML(unmarshal func(interface{}) error) error { type plain MuteTimeInterval if err := unmarshal((*plain)(mt)); err != nil { From 5983d2078dce06adf4a2169496cd4c92970ad5b4 Mon Sep 17 00:00:00 2001 From: Ben Ridley Date: Tue, 24 Nov 2020 17:58:08 +1100 Subject: [PATCH 41/47] Fix formatting Signed-off-by: Ben Ridley --- config/config.go | 10 +++++----- dispatch/route.go | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/config/config.go b/config/config.go index 2d7a780b62..76b641dc2a 100644 --- a/config/config.go +++ b/config/config.go @@ -630,11 +630,11 @@ type Route struct { GroupBy []model.LabelName `yaml:"-" json:"-"` GroupByAll bool `yaml:"-" json:"-"` - Match map[string]string `yaml:"match,omitempty" json:"match,omitempty"` - MatchRE MatchRegexps `yaml:"match_re,omitempty" json:"match_re,omitempty"` - MuteTimeIntervals []string `yaml:"mute_time_intervals,omitempty" json:"mute_time_intervals,omitempty"` - Continue bool `yaml:"continue" json:"continue,omitempty"` - Routes []*Route `yaml:"routes,omitempty" json:"routes,omitempty"` + Match map[string]string `yaml:"match,omitempty" json:"match,omitempty"` + MatchRE MatchRegexps `yaml:"match_re,omitempty" json:"match_re,omitempty"` + MuteTimeIntervals []string `yaml:"mute_time_intervals,omitempty" json:"mute_time_intervals,omitempty"` + Continue bool `yaml:"continue" json:"continue,omitempty"` + Routes []*Route `yaml:"routes,omitempty" json:"routes,omitempty"` GroupWait *model.Duration `yaml:"group_wait,omitempty" json:"group_wait,omitempty"` GroupInterval *model.Duration `yaml:"group_interval,omitempty" json:"group_interval,omitempty"` diff --git a/dispatch/route.go b/dispatch/route.go index 8708248859..f892f264b2 100644 --- a/dispatch/route.go +++ b/dispatch/route.go @@ -29,11 +29,11 @@ import ( // DefaultRouteOpts are the defaulting routing options which apply // to the root route of a routing tree. var DefaultRouteOpts = RouteOpts{ - GroupWait: 30 * time.Second, - GroupInterval: 5 * time.Minute, - RepeatInterval: 4 * time.Hour, - GroupBy: map[model.LabelName]struct{}{}, - GroupByAll: false, + GroupWait: 30 * time.Second, + GroupInterval: 5 * time.Minute, + RepeatInterval: 4 * time.Hour, + GroupBy: map[model.LabelName]struct{}{}, + GroupByAll: false, MuteTimeIntervals: []string{}, } From 253e28abde1fb6074b46102cd812c7d51a2385b0 Mon Sep 17 00:00:00 2001 From: Ben Ridley Date: Wed, 16 Dec 2020 21:32:21 +1100 Subject: [PATCH 42/47] Remove unnecessary json tag in MuteTimeInterval Signed-off-by: Ben Ridley --- config/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.go b/config/config.go index 76b641dc2a..c561110fa0 100644 --- a/config/config.go +++ b/config/config.go @@ -222,7 +222,7 @@ func resolveFilepaths(baseDir string, cfg *Config) { // MuteTimeInterval represents a named set of time intervals for which a route should be muted. type MuteTimeInterval struct { - Name string `yaml:"name" json:"name"` + Name string `yaml:"name"` TimeIntervals []timeinterval.TimeInterval `yaml:"time_intervals"` } From bcab6aa8c3c0957ddbd42e702c82d137d1bacd5a Mon Sep 17 00:00:00 2001 From: Ben Ridley Date: Wed, 16 Dec 2020 21:33:37 +1100 Subject: [PATCH 43/47] Change wording in root route mute time interval error to align with other error message wording Signed-off-by: Ben Ridley --- config/config.go | 2 +- config/config_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config/config.go b/config/config.go index c561110fa0..1d87c849b1 100644 --- a/config/config.go +++ b/config/config.go @@ -432,7 +432,7 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error { return fmt.Errorf("root route must not have any matchers") } if len(c.Route.MuteTimeIntervals) > 0 { - return fmt.Errorf("root route cannot have any mute times") + return fmt.Errorf("root route must not have any mute time intervals") } // Validate that all receivers used in the routing tree are defined. diff --git a/config/config_test.go b/config/config_test.go index 0fe587cd29..ce8c6fef10 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -347,7 +347,7 @@ route: ` _, err := Load(in) - expected := "root route cannot have any mute times" + expected := "root route must not have any mute time intervals" if err == nil { t.Fatalf("no error returned, expected:\n%q", expected) From d9d7511ad0c22e0e0d2cf5fd8a60b79a1a7ed21e Mon Sep 17 00:00:00 2001 From: Ben Ridley Date: Wed, 16 Dec 2020 21:48:08 +1100 Subject: [PATCH 44/47] Revert unwanted formatting change Signed-off-by: Ben Ridley --- config/config.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/config/config.go b/config/config.go index 1d87c849b1..485d85f156 100644 --- a/config/config.go +++ b/config/config.go @@ -487,8 +487,9 @@ func checkTimeInterval(r *Route, timeIntervals map[string]struct{}) error { // DefaultGlobalConfig returns GlobalConfig with default values. func DefaultGlobalConfig() GlobalConfig { return GlobalConfig{ - ResolveTimeout: model.Duration(5 * time.Minute), - HTTPConfig: &commoncfg.HTTPClientConfig{}, + ResolveTimeout: model.Duration(5 * time.Minute), + HTTPConfig: &commoncfg.HTTPClientConfig{}, + SMTPHello: "localhost", SMTPRequireTLS: true, PagerdutyURL: mustParseURL("https://events.pagerduty.com/v2/enqueue"), From d0217a80b0fc9957ad605eb26daa663c660ec889 Mon Sep 17 00:00:00 2001 From: Ben Ridley Date: Sun, 24 Jan 2021 20:37:46 +1100 Subject: [PATCH 45/47] Add JSON marshalling and unmarshalling support for time intervals Signed-off-by: Ben Ridley --- timeinterval/timeinterval.go | 91 ++++++++++++++++++++++++++----- timeinterval/timeinterval_test.go | 27 ++++++++- 2 files changed, 103 insertions(+), 15 deletions(-) diff --git a/timeinterval/timeinterval.go b/timeinterval/timeinterval.go index e62fb56037..24c4a4ea03 100644 --- a/timeinterval/timeinterval.go +++ b/timeinterval/timeinterval.go @@ -14,22 +14,25 @@ package timeinterval import ( + "encoding/json" "errors" "fmt" "regexp" "strconv" "strings" "time" + + "gopkg.in/yaml.v2" ) // TimeInterval describes intervals of time. ContainsTime will tell you if a golang time is contained // within the interval. type TimeInterval struct { - Times []TimeRange `yaml:"times,omitempty"` - Weekdays []WeekdayRange `yaml:"weekdays,flow,omitempty"` - DaysOfMonth []DayOfMonthRange `yaml:"days_of_month,flow,omitempty"` - Months []MonthRange `yaml:"months,flow,omitempty"` - Years []YearRange `yaml:"years,flow,omitempty"` + Times []TimeRange `yaml:"times,omitempty" json:"times,omitempty"` + Weekdays []WeekdayRange `yaml:"weekdays,flow,omitempty" json:"weekdays,omitempty"` + 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"` } // TimeRange represents a range of minutes within a 1440 minute day, exclusive of the End minute. A day consists of 1440 minutes. @@ -66,8 +69,8 @@ type YearRange struct { } type yamlTimeRange struct { - StartTime string `yaml:"start_time"` - EndTime string `yaml:"end_time"` + StartTime string `yaml:"start_time" json:"start_time"` + EndTime string `yaml:"end_time" json:"end_time"` } // A range with a Beginning and End that can be represented as strings. @@ -183,6 +186,12 @@ func (r *WeekdayRange) UnmarshalYAML(unmarshal func(interface{}) error) error { return nil } +// UnmarshalJSON implements the json.Unmarshaler interface for WeekdayRange. +// It delegates to the YAML unmarshaller as it can parse JSON and has validation logic. +func (r *WeekdayRange) UnmarshalJSON(in []byte) error { + return yaml.Unmarshal(in, r) +} + // UnmarshalYAML implements the Unmarshaller interface for DayOfMonthRange. func (r *DayOfMonthRange) UnmarshalYAML(unmarshal func(interface{}) error) error { var str string @@ -220,6 +229,12 @@ func (r *DayOfMonthRange) UnmarshalYAML(unmarshal func(interface{}) error) error return nil } +// UnmarshalJSON implements the json.Unmarshaler interface for DayOfMonthRange. +// It delegates to the YAML unmarshaller as it can parse JSON and has validation logic. +func (r *DayOfMonthRange) UnmarshalJSON(in []byte) error { + return yaml.Unmarshal(in, r) +} + // UnmarshalYAML implements the Unmarshaller interface for MonthRange. func (r *MonthRange) UnmarshalYAML(unmarshal func(interface{}) error) error { var str string @@ -237,6 +252,12 @@ func (r *MonthRange) UnmarshalYAML(unmarshal func(interface{}) error) error { return nil } +// UnmarshalJSON implements the json.Unmarshaler interface for MonthRange. +// It delegates to the YAML unmarshaller as it can parse JSON and has validation logic. +func (r *MonthRange) UnmarshalJSON(in []byte) error { + return yaml.Unmarshal(in, r) +} + // UnmarshalYAML implements the Unmarshaller interface for YearRange. func (r *YearRange) UnmarshalYAML(unmarshal func(interface{}) error) error { var str string @@ -252,6 +273,12 @@ func (r *YearRange) UnmarshalYAML(unmarshal func(interface{}) error) error { return nil } +// UnmarshalJSON implements the json.Unmarshaler interface for YearRange. +// It delegates to the YAML unmarshaller as it can parse JSON and has validation logic. +func (r *YearRange) UnmarshalJSON(in []byte) error { + return yaml.Unmarshal(in, r) +} + // UnmarshalYAML implements the Unmarshaller interface for TimeRanges. func (tr *TimeRange) UnmarshalYAML(unmarshal func(interface{}) error) error { var y yamlTimeRange @@ -276,24 +303,38 @@ func (tr *TimeRange) UnmarshalYAML(unmarshal func(interface{}) error) error { return nil } +// UnmarshalJSON implements the json.Unmarshaler interface for Timerange. +// It delegates to the YAML unmarshaller as it can parse JSON and has validation logic. +func (tr *TimeRange) UnmarshalJSON(in []byte) error { + return yaml.Unmarshal(in, tr) +} + // MarshalYAML implements the yaml.Marshaler interface for WeekdayRange. func (r WeekdayRange) MarshalYAML() (interface{}, error) { + bytes, err := r.MarshalText() + return string(bytes), err +} + +// MarshalText implements the econding.TextMarshaler interface for WeekdayRange. +// It converts the range into a colon-seperated string, or a single weekday if possible. +// e.g. "monday:friday" or "saturday". +func (r WeekdayRange) MarshalText() ([]byte, error) { beginStr, ok := daysOfWeekInv[r.Begin] if !ok { return nil, fmt.Errorf("unable to convert %d into weekday string", r.Begin) } if r.Begin == r.End { - return interface{}(beginStr), nil + return []byte(beginStr), nil } endStr, ok := daysOfWeekInv[r.End] if !ok { return nil, fmt.Errorf("unable to convert %d into weekday string", r.End) } rangeStr := fmt.Sprintf("%s:%s", beginStr, endStr) - return interface{}(rangeStr), nil + return []byte(rangeStr), nil } -//MarshalYAML implements the yaml.Marshaler interface for TimeRange. +// MarshalYAML implements the yaml.Marshaler interface for TimeRange. func (tr TimeRange) MarshalYAML() (out interface{}, err error) { startHr := tr.StartMinute / 60 endHr := tr.EndMinute / 60 @@ -307,13 +348,35 @@ func (tr TimeRange) MarshalYAML() (out interface{}, err error) { return interface{}(yTr), err } -//MarshalYAML implements the yaml.Marshaler interface for InclusiveRange. -func (ir InclusiveRange) MarshalYAML() (interface{}, error) { +// MarshalJSON implements the json.Marshaler interface for TimeRange. +func (tr TimeRange) MarshalJSON() (out []byte, err error) { + startHr := tr.StartMinute / 60 + endHr := tr.EndMinute / 60 + startMin := tr.StartMinute % 60 + endMin := tr.EndMinute % 60 + + startStr := fmt.Sprintf("%02d:%02d", startHr, startMin) + endStr := fmt.Sprintf("%02d:%02d", endHr, endMin) + + yTr := yamlTimeRange{startStr, endStr} + return json.Marshal(yTr) +} + +// 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" +func (ir InclusiveRange) MarshalText() ([]byte, error) { if ir.Begin == ir.End { - return strconv.Itoa(ir.Begin), nil + return []byte(strconv.Itoa(ir.Begin)), nil } out := fmt.Sprintf("%d:%d", ir.Begin, ir.End) - return interface{}(out), nil + return []byte(out), nil +} + +//MarshalYAML implements the yaml.Marshaler interface for InclusiveRange. +func (ir InclusiveRange) MarshalYAML() (interface{}, error) { + bytes, err := ir.MarshalText() + return string(bytes), err } // TimeLayout specifies the layout to be used in time.Parse() calls for time intervals. diff --git a/timeinterval/timeinterval_test.go b/timeinterval/timeinterval_test.go index 6ca932de63..47b8e0b849 100644 --- a/timeinterval/timeinterval_test.go +++ b/timeinterval/timeinterval_test.go @@ -14,6 +14,7 @@ package timeinterval import ( + "encoding/json" "reflect" "testing" "time" @@ -507,7 +508,31 @@ func TestYamlMarshal(t *testing.T) { var ti2 []TimeInterval yaml.Unmarshal(out, &ti2) if !reflect.DeepEqual(ti, ti2) { - t.Errorf("Re-marshalling %s produced a different TimeInterval", tc.in) + t.Errorf("Re-marshalling %s produced a different TimeInterval.", tc.in) + } + } +} + +// Test JSON marshalling by marshalling a time interval +// and then unmarshalling to ensure they're identical +func TestJsonMarshal(t *testing.T) { + for _, tc := range yamlUnmarshalTestCases { + if tc.expectError { + continue + } + var ti []TimeInterval + err := yaml.Unmarshal([]byte(tc.in), &ti) + if err != nil { + t.Error(err) + } + out, err := json.Marshal(&ti) + if err != nil { + t.Error(err) + } + var ti2 []TimeInterval + json.Unmarshal(out, &ti2) + if !reflect.DeepEqual(ti, ti2) { + t.Errorf("Re-marshalling %s produced a different TimeInterval. Used:\n%s and got:\n%v", tc.in, out, ti2) } } } From 3a5f4e5043ff98f831638b3bda78f02024be0649 Mon Sep 17 00:00:00 2001 From: Ben Ridley Date: Sun, 24 Jan 2021 20:40:01 +1100 Subject: [PATCH 46/47] Fix formatting after merge conflict in config.go Signed-off-by: Ben Ridley --- config/config.go | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/config/config.go b/config/config.go index 485d85f156..f83dc835d6 100644 --- a/config/config.go +++ b/config/config.go @@ -630,12 +630,14 @@ type Route struct { GroupByStr []string `yaml:"group_by,omitempty" json:"group_by,omitempty"` GroupBy []model.LabelName `yaml:"-" json:"-"` GroupByAll bool `yaml:"-" json:"-"` - - Match map[string]string `yaml:"match,omitempty" json:"match,omitempty"` - MatchRE MatchRegexps `yaml:"match_re,omitempty" json:"match_re,omitempty"` - MuteTimeIntervals []string `yaml:"mute_time_intervals,omitempty" json:"mute_time_intervals,omitempty"` - Continue bool `yaml:"continue" json:"continue,omitempty"` - Routes []*Route `yaml:"routes,omitempty" json:"routes,omitempty"` + // Deprecated. Remove before v1.0 release. + Match map[string]string `yaml:"match,omitempty" json:"match,omitempty"` + // Deprecated. Remove before v1.0 release. + MatchRE MatchRegexps `yaml:"match_re,omitempty" json:"match_re,omitempty"` + Matchers Matchers `yaml:"matchers,omitempty" json:"matchers,omitempty"` + MuteTimeIntervals []string `yaml:"mute_time_intervals,omitempty" json:"mute_time_intervals,omitempty"` + Continue bool `yaml:"continue" json:"continue,omitempty"` + Routes []*Route `yaml:"routes,omitempty" json:"routes,omitempty"` GroupWait *model.Duration `yaml:"group_wait,omitempty" json:"group_wait,omitempty"` GroupInterval *model.Duration `yaml:"group_interval,omitempty" json:"group_interval,omitempty"` From df54b4bacf3f3bd3fa7612394bd28d10d1e69a2d Mon Sep 17 00:00:00 2001 From: Ben Ridley Date: Tue, 16 Feb 2021 21:45:21 +1100 Subject: [PATCH 47/47] Improve documentation wording and formatting in response to maintainer feedback Signed-off-by: Ben Ridley --- config/config.go | 2 +- docs/configuration.md | 21 +++++++++++++-------- notify/notify_test.go | 2 +- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/config/config.go b/config/config.go index f83dc835d6..440b505f33 100644 --- a/config/config.go +++ b/config/config.go @@ -26,12 +26,12 @@ import ( "time" "github.com/pkg/errors" - "github.com/prometheus/alertmanager/timeinterval" commoncfg "github.com/prometheus/common/config" "github.com/prometheus/common/model" "gopkg.in/yaml.v2" "github.com/prometheus/alertmanager/pkg/labels" + "github.com/prometheus/alertmanager/timeinterval" ) const secretToken = "" diff --git a/docs/configuration.md b/docs/configuration.md index 046affc456..96cac65246 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -175,6 +175,9 @@ match_re: # Times when the route should be muted. These must match the name of a # mute time interval defined in the mute_time_intervals section. # Additionally, the root node cannot have any mute times. +# When a route is muted it will not send any notifications, but +# otherwise acts normally (including ending the route-matching process +# if the `continue` option is not set.) mute_time_intervals: [ - ...] @@ -239,36 +242,38 @@ supports the following fields: [ - ...] ``` -All these fields are optional and if left unspecified allow any value to match the interval. +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. -`times`: A list of time-ranges. They are inclusive of the starting time and exclusive -of the ending time to make it easy to represent times that start/end on hour boundaries. +`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. For example, start_time: '17:00' and end_time: '24:00' will begin at 17:00 and finish -immediately after 23:59. They are specified like so: +immediately before 24:00. They are specified like so: times: - start_time: HH:MM end_time: HH:MM -`weekdays`: A list of days of the week, where the week begins on Sunday and ends on Saturday. +`weeekday_range`: A list of days of the week, where the week begins on Sunday and ends on Saturday. Days should be specified by name (e.g. ‘Sunday’). For convenience, ranges are also accepted of the form : and are inclusive on both ends. For example: `[‘monday:wednesday','saturday', 'sunday']` -`days_of_month`: A list of numerical days in the month. Days begin at 1. +`days_of_month_ramge`: A list of numerical days in the month. Days begin at 1. Negative values are also accepted which begin at the end of the month, e.g. -1 during January would represent January 31. For example: `['1:5', '-3:-1']`. Extending past the start or end of the month will cause it to be clamped. E.g. specifying `['1:31']` during February will clamp the actual end date to 28 or 29 depending on leap years. Inclusive on both ends. -`months`: A list of calendar months identified by a case-insentive name (e.g. ‘January’) or by number, +`month_range`: A list of calendar months identified by a case-insentive name (e.g. ‘January’) or by number, where January = 1. Ranges are also accepted. For example, `['1:3', 'may:august', 'december']`. Inclusive on both ends. -`years`: A numerical list of years. Ranges are accepted. For example, `['2020:2022', '2030']`. +`year_range`: A numerical list of years. Ranges are accepted. For example, `['2020:2022', '2030']`. Inclusive on both ends. ## `` diff --git a/notify/notify_test.go b/notify/notify_test.go index 660c37095c..94e1a14023 100644 --- a/notify/notify_test.go +++ b/notify/notify_test.go @@ -723,7 +723,7 @@ func TestMuteStageWithSilences(t *testing.T) { } func TestTimeMuteStage(t *testing.T) { - // Route mutes alerts outside business hours + // Route mutes alerts outside business hours. muteIn := ` --- - weekdays: ['monday:friday']