...

Source file src/github.com/prometheus/alertmanager/timeinterval/timeinterval.go

Documentation: github.com/prometheus/alertmanager/timeinterval

     1  // Copyright 2020 Prometheus Team
     2  // Licensed under the Apache License, Version 2.0 (the "License");
     3  // you may not use this file except in compliance with the License.
     4  // You may obtain a copy of the License at
     5  //
     6  // http://www.apache.org/licenses/LICENSE-2.0
     7  //
     8  // Unless required by applicable law or agreed to in writing, software
     9  // distributed under the License is distributed on an "AS IS" BASIS,
    10  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    11  // See the License for the specific language governing permissions and
    12  // limitations under the License.
    13  
    14  package timeinterval
    15  
    16  import (
    17  	"encoding/json"
    18  	"errors"
    19  	"fmt"
    20  	"os"
    21  	"regexp"
    22  	"runtime"
    23  	"strconv"
    24  	"strings"
    25  	"time"
    26  
    27  	"gopkg.in/yaml.v2"
    28  )
    29  
    30  // TimeInterval describes intervals of time. ContainsTime will tell you if a golang time is contained
    31  // within the interval.
    32  type TimeInterval struct {
    33  	Times       []TimeRange       `yaml:"times,omitempty" json:"times,omitempty"`
    34  	Weekdays    []WeekdayRange    `yaml:"weekdays,flow,omitempty" json:"weekdays,omitempty"`
    35  	DaysOfMonth []DayOfMonthRange `yaml:"days_of_month,flow,omitempty" json:"days_of_month,omitempty"`
    36  	Months      []MonthRange      `yaml:"months,flow,omitempty" json:"months,omitempty"`
    37  	Years       []YearRange       `yaml:"years,flow,omitempty" json:"years,omitempty"`
    38  	Location    *Location         `yaml:"location,flow,omitempty" json:"location,omitempty"`
    39  }
    40  
    41  // TimeRange represents a range of minutes within a 1440 minute day, exclusive of the End minute. A day consists of 1440 minutes.
    42  // For example, 4:00PM to End of the day would Begin at 1020 and End at 1440.
    43  type TimeRange struct {
    44  	StartMinute int
    45  	EndMinute   int
    46  }
    47  
    48  // InclusiveRange is used to hold the Beginning and End values of many time interval components.
    49  type InclusiveRange struct {
    50  	Begin int
    51  	End   int
    52  }
    53  
    54  // A WeekdayRange is an inclusive range between [0, 6] where 0 = Sunday.
    55  type WeekdayRange struct {
    56  	InclusiveRange
    57  }
    58  
    59  // 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.
    60  type DayOfMonthRange struct {
    61  	InclusiveRange
    62  }
    63  
    64  // A MonthRange is an inclusive range between [1, 12] where 1 = January.
    65  type MonthRange struct {
    66  	InclusiveRange
    67  }
    68  
    69  // A YearRange is a positive inclusive range.
    70  type YearRange struct {
    71  	InclusiveRange
    72  }
    73  
    74  // A Location is a container for a time.Location, used for custom unmarshalling/validation logic.
    75  type Location struct {
    76  	*time.Location
    77  }
    78  
    79  type yamlTimeRange struct {
    80  	StartTime string `yaml:"start_time" json:"start_time"`
    81  	EndTime   string `yaml:"end_time" json:"end_time"`
    82  }
    83  
    84  // A range with a Beginning and End that can be represented as strings.
    85  type stringableRange interface {
    86  	setBegin(int)
    87  	setEnd(int)
    88  	// Try to map a member of the range into an integer.
    89  	memberFromString(string) (int, error)
    90  }
    91  
    92  func (ir *InclusiveRange) setBegin(n int) {
    93  	ir.Begin = n
    94  }
    95  
    96  func (ir *InclusiveRange) setEnd(n int) {
    97  	ir.End = n
    98  }
    99  
   100  func (ir *InclusiveRange) memberFromString(in string) (out int, err error) {
   101  	out, err = strconv.Atoi(in)
   102  	if err != nil {
   103  		return -1, err
   104  	}
   105  	return out, nil
   106  }
   107  
   108  func (r *WeekdayRange) memberFromString(in string) (out int, err error) {
   109  	out, ok := daysOfWeek[in]
   110  	if !ok {
   111  		return -1, fmt.Errorf("%s is not a valid weekday", in)
   112  	}
   113  	return out, nil
   114  }
   115  
   116  func (r *MonthRange) memberFromString(in string) (out int, err error) {
   117  	out, ok := months[in]
   118  	if !ok {
   119  		out, err = strconv.Atoi(in)
   120  		if err != nil {
   121  			return -1, fmt.Errorf("%s is not a valid month", in)
   122  		}
   123  	}
   124  	return out, nil
   125  }
   126  
   127  var daysOfWeek = map[string]int{
   128  	"sunday":    0,
   129  	"monday":    1,
   130  	"tuesday":   2,
   131  	"wednesday": 3,
   132  	"thursday":  4,
   133  	"friday":    5,
   134  	"saturday":  6,
   135  }
   136  
   137  var daysOfWeekInv = map[int]string{
   138  	0: "sunday",
   139  	1: "monday",
   140  	2: "tuesday",
   141  	3: "wednesday",
   142  	4: "thursday",
   143  	5: "friday",
   144  	6: "saturday",
   145  }
   146  
   147  var months = map[string]int{
   148  	"january":   1,
   149  	"february":  2,
   150  	"march":     3,
   151  	"april":     4,
   152  	"may":       5,
   153  	"june":      6,
   154  	"july":      7,
   155  	"august":    8,
   156  	"september": 9,
   157  	"october":   10,
   158  	"november":  11,
   159  	"december":  12,
   160  }
   161  
   162  var monthsInv = map[int]string{
   163  	1:  "january",
   164  	2:  "february",
   165  	3:  "march",
   166  	4:  "april",
   167  	5:  "may",
   168  	6:  "june",
   169  	7:  "july",
   170  	8:  "august",
   171  	9:  "september",
   172  	10: "october",
   173  	11: "november",
   174  	12: "december",
   175  }
   176  
   177  // UnmarshalYAML implements the Unmarshaller interface for Location.
   178  func (tz *Location) UnmarshalYAML(unmarshal func(interface{}) error) error {
   179  	var str string
   180  	if err := unmarshal(&str); err != nil {
   181  		return err
   182  	}
   183  
   184  	loc, err := time.LoadLocation(str)
   185  	if err != nil {
   186  		if runtime.GOOS == "windows" {
   187  			if zoneinfo := os.Getenv("ZONEINFO"); zoneinfo != "" {
   188  				return fmt.Errorf("%w (ZONEINFO=%q)", err, zoneinfo)
   189  			}
   190  			return fmt.Errorf("%w (on Windows platforms, you may have to pass the time zone database using the ZONEINFO environment variable, see https://pkg.go.dev/time#LoadLocation for details)", err)
   191  		}
   192  		return err
   193  	}
   194  
   195  	*tz = Location{loc}
   196  	return nil
   197  }
   198  
   199  // UnmarshalJSON implements the json.Unmarshaler interface for Location.
   200  // It delegates to the YAML unmarshaller as it can parse JSON and has validation logic.
   201  func (tz *Location) UnmarshalJSON(in []byte) error {
   202  	return yaml.Unmarshal(in, tz)
   203  }
   204  
   205  // UnmarshalYAML implements the Unmarshaller interface for WeekdayRange.
   206  func (r *WeekdayRange) UnmarshalYAML(unmarshal func(interface{}) error) error {
   207  	var str string
   208  	if err := unmarshal(&str); err != nil {
   209  		return err
   210  	}
   211  	if err := stringableRangeFromString(str, r); err != nil {
   212  		return err
   213  	}
   214  	if r.Begin > r.End {
   215  		return errors.New("start day cannot be before end day")
   216  	}
   217  	if r.Begin < 0 || r.Begin > 6 {
   218  		return fmt.Errorf("%s is not a valid day of the week: out of range", str)
   219  	}
   220  	if r.End < 0 || r.End > 6 {
   221  		return fmt.Errorf("%s is not a valid day of the week: out of range", str)
   222  	}
   223  	return nil
   224  }
   225  
   226  // UnmarshalJSON implements the json.Unmarshaler interface for WeekdayRange.
   227  // It delegates to the YAML unmarshaller as it can parse JSON and has validation logic.
   228  func (r *WeekdayRange) UnmarshalJSON(in []byte) error {
   229  	return yaml.Unmarshal(in, r)
   230  }
   231  
   232  // UnmarshalYAML implements the Unmarshaller interface for DayOfMonthRange.
   233  func (r *DayOfMonthRange) UnmarshalYAML(unmarshal func(interface{}) error) error {
   234  	var str string
   235  	if err := unmarshal(&str); err != nil {
   236  		return err
   237  	}
   238  	if err := stringableRangeFromString(str, r); err != nil {
   239  		return err
   240  	}
   241  	// Check beginning <= end accounting for negatives day of month indices as well.
   242  	// Months != 31 days can't be addressed here and are clamped, but at least we can catch blatant errors.
   243  	if r.Begin == 0 || r.Begin < -31 || r.Begin > 31 {
   244  		return fmt.Errorf("%d is not a valid day of the month: out of range", r.Begin)
   245  	}
   246  	if r.End == 0 || r.End < -31 || r.End > 31 {
   247  		return fmt.Errorf("%d is not a valid day of the month: out of range", r.End)
   248  	}
   249  	// Restricting here prevents errors where begin > end in longer months but not shorter months.
   250  	if r.Begin < 0 && r.End > 0 {
   251  		return fmt.Errorf("end day must be negative if start day is negative")
   252  	}
   253  	// Check begin <= end. We can't know this for sure when using negative indices
   254  	// but we can prevent cases where its always invalid (using 28 day minimum length).
   255  	checkBegin := r.Begin
   256  	checkEnd := r.End
   257  	if r.Begin < 0 {
   258  		checkBegin = 28 + r.Begin
   259  	}
   260  	if r.End < 0 {
   261  		checkEnd = 28 + r.End
   262  	}
   263  	if checkBegin > checkEnd {
   264  		return fmt.Errorf("end day %d is always before start day %d", r.End, r.Begin)
   265  	}
   266  	return nil
   267  }
   268  
   269  // UnmarshalJSON implements the json.Unmarshaler interface for DayOfMonthRange.
   270  // It delegates to the YAML unmarshaller as it can parse JSON and has validation logic.
   271  func (r *DayOfMonthRange) UnmarshalJSON(in []byte) error {
   272  	return yaml.Unmarshal(in, r)
   273  }
   274  
   275  // UnmarshalYAML implements the Unmarshaller interface for MonthRange.
   276  func (r *MonthRange) UnmarshalYAML(unmarshal func(interface{}) error) error {
   277  	var str string
   278  	if err := unmarshal(&str); err != nil {
   279  		return err
   280  	}
   281  	if err := stringableRangeFromString(str, r); err != nil {
   282  		return err
   283  	}
   284  	if r.Begin > r.End {
   285  		begin := monthsInv[r.Begin]
   286  		end := monthsInv[r.End]
   287  		return fmt.Errorf("end month %s is before start month %s", end, begin)
   288  	}
   289  	return nil
   290  }
   291  
   292  // UnmarshalJSON implements the json.Unmarshaler interface for MonthRange.
   293  // It delegates to the YAML unmarshaller as it can parse JSON and has validation logic.
   294  func (r *MonthRange) UnmarshalJSON(in []byte) error {
   295  	return yaml.Unmarshal(in, r)
   296  }
   297  
   298  // UnmarshalYAML implements the Unmarshaller interface for YearRange.
   299  func (r *YearRange) UnmarshalYAML(unmarshal func(interface{}) error) error {
   300  	var str string
   301  	if err := unmarshal(&str); err != nil {
   302  		return err
   303  	}
   304  	if err := stringableRangeFromString(str, r); err != nil {
   305  		return err
   306  	}
   307  	if r.Begin > r.End {
   308  		return fmt.Errorf("end year %d is before start year %d", r.End, r.Begin)
   309  	}
   310  	return nil
   311  }
   312  
   313  // UnmarshalJSON implements the json.Unmarshaler interface for YearRange.
   314  // It delegates to the YAML unmarshaller as it can parse JSON and has validation logic.
   315  func (r *YearRange) UnmarshalJSON(in []byte) error {
   316  	return yaml.Unmarshal(in, r)
   317  }
   318  
   319  // UnmarshalYAML implements the Unmarshaller interface for TimeRanges.
   320  func (tr *TimeRange) UnmarshalYAML(unmarshal func(interface{}) error) error {
   321  	var y yamlTimeRange
   322  	if err := unmarshal(&y); err != nil {
   323  		return err
   324  	}
   325  	if y.EndTime == "" || y.StartTime == "" {
   326  		return errors.New("both start and end times must be provided")
   327  	}
   328  	start, err := parseTime(y.StartTime)
   329  	if err != nil {
   330  		return err
   331  	}
   332  	end, err := parseTime(y.EndTime)
   333  	if err != nil {
   334  		return err
   335  	}
   336  	if start >= end {
   337  		return errors.New("start time cannot be equal or greater than end time")
   338  	}
   339  	tr.StartMinute, tr.EndMinute = start, end
   340  	return nil
   341  }
   342  
   343  // UnmarshalJSON implements the json.Unmarshaler interface for Timerange.
   344  // It delegates to the YAML unmarshaller as it can parse JSON and has validation logic.
   345  func (tr *TimeRange) UnmarshalJSON(in []byte) error {
   346  	return yaml.Unmarshal(in, tr)
   347  }
   348  
   349  // MarshalYAML implements the yaml.Marshaler interface for WeekdayRange.
   350  func (r WeekdayRange) MarshalYAML() (interface{}, error) {
   351  	bytes, err := r.MarshalText()
   352  	return string(bytes), err
   353  }
   354  
   355  // MarshalText implements the econding.TextMarshaler interface for WeekdayRange.
   356  // It converts the range into a colon-separated string, or a single weekday if possible.
   357  // e.g. "monday:friday" or "saturday".
   358  func (r WeekdayRange) MarshalText() ([]byte, error) {
   359  	beginStr, ok := daysOfWeekInv[r.Begin]
   360  	if !ok {
   361  		return nil, fmt.Errorf("unable to convert %d into weekday string", r.Begin)
   362  	}
   363  	if r.Begin == r.End {
   364  		return []byte(beginStr), nil
   365  	}
   366  	endStr, ok := daysOfWeekInv[r.End]
   367  	if !ok {
   368  		return nil, fmt.Errorf("unable to convert %d into weekday string", r.End)
   369  	}
   370  	rangeStr := fmt.Sprintf("%s:%s", beginStr, endStr)
   371  	return []byte(rangeStr), nil
   372  }
   373  
   374  // MarshalYAML implements the yaml.Marshaler interface for TimeRange.
   375  func (tr TimeRange) MarshalYAML() (out interface{}, err error) {
   376  	startHr := tr.StartMinute / 60
   377  	endHr := tr.EndMinute / 60
   378  	startMin := tr.StartMinute % 60
   379  	endMin := tr.EndMinute % 60
   380  
   381  	startStr := fmt.Sprintf("%02d:%02d", startHr, startMin)
   382  	endStr := fmt.Sprintf("%02d:%02d", endHr, endMin)
   383  
   384  	yTr := yamlTimeRange{startStr, endStr}
   385  	return interface{}(yTr), err
   386  }
   387  
   388  // MarshalJSON implements the json.Marshaler interface for TimeRange.
   389  func (tr TimeRange) MarshalJSON() (out []byte, err error) {
   390  	startHr := tr.StartMinute / 60
   391  	endHr := tr.EndMinute / 60
   392  	startMin := tr.StartMinute % 60
   393  	endMin := tr.EndMinute % 60
   394  
   395  	startStr := fmt.Sprintf("%02d:%02d", startHr, startMin)
   396  	endStr := fmt.Sprintf("%02d:%02d", endHr, endMin)
   397  
   398  	yTr := yamlTimeRange{startStr, endStr}
   399  	return json.Marshal(yTr)
   400  }
   401  
   402  // MarshalText implements the econding.TextMarshaler interface for Location.
   403  // It marshals a Location back into a string that represents a time.Location.
   404  func (tz Location) MarshalText() ([]byte, error) {
   405  	if tz.Location == nil {
   406  		return nil, fmt.Errorf("unable to convert nil location into string")
   407  	}
   408  	return []byte(tz.Location.String()), nil
   409  }
   410  
   411  // MarshalYAML implements the yaml.Marshaler interface for Location.
   412  func (tz Location) MarshalYAML() (interface{}, error) {
   413  	bytes, err := tz.MarshalText()
   414  	return string(bytes), err
   415  }
   416  
   417  // MarshalJSON implements the json.Marshaler interface for Location.
   418  func (tz Location) MarshalJSON() (out []byte, err error) {
   419  	return json.Marshal(tz.String())
   420  }
   421  
   422  // MarshalText implements the encoding.TextMarshaler interface for InclusiveRange.
   423  // It converts the struct into a colon-separated string, or a single element if
   424  // appropriate. e.g. "monday:friday" or "monday"
   425  func (ir InclusiveRange) MarshalText() ([]byte, error) {
   426  	if ir.Begin == ir.End {
   427  		return []byte(strconv.Itoa(ir.Begin)), nil
   428  	}
   429  	out := fmt.Sprintf("%d:%d", ir.Begin, ir.End)
   430  	return []byte(out), nil
   431  }
   432  
   433  // MarshalYAML implements the yaml.Marshaler interface for InclusiveRange.
   434  func (ir InclusiveRange) MarshalYAML() (interface{}, error) {
   435  	bytes, err := ir.MarshalText()
   436  	return string(bytes), err
   437  }
   438  
   439  // TimeLayout specifies the layout to be used in time.Parse() calls for time intervals.
   440  const TimeLayout = "15:04"
   441  
   442  var (
   443  	validTime   = "^((([01][0-9])|(2[0-3])):[0-5][0-9])$|(^24:00$)"
   444  	validTimeRE = regexp.MustCompile(validTime)
   445  )
   446  
   447  // Given a time, determines the number of days in the month that time occurs in.
   448  func daysInMonth(t time.Time) int {
   449  	monthStart := time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, t.Location())
   450  	monthEnd := monthStart.AddDate(0, 1, 0)
   451  	diff := monthEnd.Sub(monthStart)
   452  	return int(diff.Hours() / 24)
   453  }
   454  
   455  func clamp(n, min, max int) int {
   456  	if n <= min {
   457  		return min
   458  	}
   459  	if n >= max {
   460  		return max
   461  	}
   462  	return n
   463  }
   464  
   465  // ContainsTime returns true if the TimeInterval contains the given time, otherwise returns false.
   466  func (tp TimeInterval) ContainsTime(t time.Time) bool {
   467  	if tp.Location != nil {
   468  		t = t.In(tp.Location.Location)
   469  	}
   470  	if tp.Times != nil {
   471  		in := false
   472  		for _, validMinutes := range tp.Times {
   473  			if (t.Hour()*60+t.Minute()) >= validMinutes.StartMinute && (t.Hour()*60+t.Minute()) < validMinutes.EndMinute {
   474  				in = true
   475  				break
   476  			}
   477  		}
   478  		if !in {
   479  			return false
   480  		}
   481  	}
   482  	if tp.DaysOfMonth != nil {
   483  		in := false
   484  		for _, validDates := range tp.DaysOfMonth {
   485  			var begin, end int
   486  			daysInMonth := daysInMonth(t)
   487  			if validDates.Begin < 0 {
   488  				begin = daysInMonth + validDates.Begin + 1
   489  			} else {
   490  				begin = validDates.Begin
   491  			}
   492  			if validDates.End < 0 {
   493  				end = daysInMonth + validDates.End + 1
   494  			} else {
   495  				end = validDates.End
   496  			}
   497  			// Skip clamping if the beginning date is after the end of the month.
   498  			if begin > daysInMonth {
   499  				continue
   500  			}
   501  			// Clamp to the boundaries of the month to prevent crossing into other months.
   502  			begin = clamp(begin, -1*daysInMonth, daysInMonth)
   503  			end = clamp(end, -1*daysInMonth, daysInMonth)
   504  			if t.Day() >= begin && t.Day() <= end {
   505  				in = true
   506  				break
   507  			}
   508  		}
   509  		if !in {
   510  			return false
   511  		}
   512  	}
   513  	if tp.Months != nil {
   514  		in := false
   515  		for _, validMonths := range tp.Months {
   516  			if t.Month() >= time.Month(validMonths.Begin) && t.Month() <= time.Month(validMonths.End) {
   517  				in = true
   518  				break
   519  			}
   520  		}
   521  		if !in {
   522  			return false
   523  		}
   524  	}
   525  	if tp.Weekdays != nil {
   526  		in := false
   527  		for _, validDays := range tp.Weekdays {
   528  			if t.Weekday() >= time.Weekday(validDays.Begin) && t.Weekday() <= time.Weekday(validDays.End) {
   529  				in = true
   530  				break
   531  			}
   532  		}
   533  		if !in {
   534  			return false
   535  		}
   536  	}
   537  	if tp.Years != nil {
   538  		in := false
   539  		for _, validYears := range tp.Years {
   540  			if t.Year() >= validYears.Begin && t.Year() <= validYears.End {
   541  				in = true
   542  				break
   543  			}
   544  		}
   545  		if !in {
   546  			return false
   547  		}
   548  	}
   549  	return true
   550  }
   551  
   552  // Converts a string of the form "HH:MM" into the number of minutes elapsed in the day.
   553  func parseTime(in string) (mins int, err error) {
   554  	if !validTimeRE.MatchString(in) {
   555  		return 0, fmt.Errorf("couldn't parse timestamp %s, invalid format", in)
   556  	}
   557  	timestampComponents := strings.Split(in, ":")
   558  	if len(timestampComponents) != 2 {
   559  		return 0, fmt.Errorf("invalid timestamp format: %s", in)
   560  	}
   561  	timeStampHours, err := strconv.Atoi(timestampComponents[0])
   562  	if err != nil {
   563  		return 0, err
   564  	}
   565  	timeStampMinutes, err := strconv.Atoi(timestampComponents[1])
   566  	if err != nil {
   567  		return 0, err
   568  	}
   569  	if timeStampHours < 0 || timeStampHours > 24 || timeStampMinutes < 0 || timeStampMinutes > 60 {
   570  		return 0, fmt.Errorf("timestamp %s out of range", in)
   571  	}
   572  	// Timestamps are stored as minutes elapsed in the day, so multiply hours by 60.
   573  	mins = timeStampHours*60 + timeStampMinutes
   574  	return mins, nil
   575  }
   576  
   577  // Converts a range that can be represented as strings (e.g. monday:wednesday) into an equivalent integer-represented range.
   578  func stringableRangeFromString(in string, r stringableRange) (err error) {
   579  	in = strings.ToLower(in)
   580  	if strings.ContainsRune(in, ':') {
   581  		components := strings.Split(in, ":")
   582  		if len(components) != 2 {
   583  			return fmt.Errorf("couldn't parse range %s, invalid format", in)
   584  		}
   585  		start, err := r.memberFromString(components[0])
   586  		if err != nil {
   587  			return err
   588  		}
   589  		End, err := r.memberFromString(components[1])
   590  		if err != nil {
   591  			return err
   592  		}
   593  		r.setBegin(start)
   594  		r.setEnd(End)
   595  		return nil
   596  	}
   597  	val, err := r.memberFromString(in)
   598  	if err != nil {
   599  		return err
   600  	}
   601  	r.setBegin(val)
   602  	r.setEnd(val)
   603  	return nil
   604  }
   605  

View as plain text