From af00dfdb7394fbe0a1a738b93b962101bbf97f3c Mon Sep 17 00:00:00 2001 From: lefes Date: Tue, 2 Jul 2024 02:41:33 +0300 Subject: [PATCH] feat(alerting): add timezone for maintenance (#653) * feat(alerting): add timezone for maintenance * Update config/maintenance/maintenance.go * docs: Add example of maintenance.timezone in readme.md * fix: Only set time to timezone location if the location is set * fix: Include the original error in the message --------- Co-authored-by: TwiN --- README.md | 5 +- config/maintenance/maintenance.go | 19 ++++++- config/maintenance/maintenance_test.go | 68 +++++++++++++++++++++++++- 3 files changed, 86 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 90409b86..5d2a436e 100644 --- a/README.md +++ b/README.md @@ -1459,15 +1459,15 @@ To do that, you'll have to use the maintenance configuration: | `maintenance.enabled` | Whether the maintenance period is enabled | `true` | | `maintenance.start` | Time at which the maintenance window starts in `hh:mm` format (e.g. `23:00`) | Required `""` | | `maintenance.duration` | Duration of the maintenance window (e.g. `1h`, `30m`) | Required `""` | +| `maintenance.timezone` | Timezone of the maintenance window format (e.g. `Europe/Amsterdam`).
See [List of tz database time zones](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) for more info | `UTC` | | `maintenance.every` | Days on which the maintenance period applies (e.g. `[Monday, Thursday]`).
If left empty, the maintenance window applies every day | `[]` | -> 📝 The maintenance configuration uses UTC - Here's an example: ```yaml maintenance: start: 23:00 duration: 1h + timezone: "Europe/Amsterdam" every: [Monday, Thursday] ``` Note that you can also specify each day on separate lines: @@ -1475,6 +1475,7 @@ Note that you can also specify each day on separate lines: maintenance: start: 23:00 duration: 1h + timezone: "Europe/Amsterdam" every: - Monday - Thursday diff --git a/config/maintenance/maintenance.go b/config/maintenance/maintenance.go index dc66fefe..291f0d11 100644 --- a/config/maintenance/maintenance.go +++ b/config/maintenance/maintenance.go @@ -12,6 +12,7 @@ var ( errInvalidMaintenanceStartFormat = errors.New("invalid maintenance start format: must be hh:mm, between 00:00 and 23:59 inclusively (e.g. 23:00)") errInvalidMaintenanceDuration = errors.New("invalid maintenance duration: must be bigger than 0 (e.g. 30m)") errInvalidDayName = fmt.Errorf("invalid value specified for 'on'. supported values are %s", longDayNames) + errInvalidTimezone = errors.New("invalid timezone specified or format not supported. Use IANA timezone format (e.g. America/Sao_Paulo)") longDayNames = []string{ "Sunday", @@ -27,17 +28,19 @@ var ( // Config allows for the configuration of a maintenance period. // During this maintenance period, no alerts will be sent. // -// Uses UTC. +// Uses UTC by default. type Config struct { Enabled *bool `yaml:"enabled"` // Whether the maintenance period is enabled. Enabled by default if nil. Start string `yaml:"start"` // Time at which the maintenance period starts (e.g. 23:00) Duration time.Duration `yaml:"duration"` // Duration of the maintenance period (e.g. 4h) + Timezone string `yaml:"timezone"` // Timezone in string format which the maintenance period is configured (e.g. America/Sao_Paulo) // Every is a list of days of the week during which maintenance period applies. // See longDayNames for list of valid values. // Every day if empty. Every []string `yaml:"every"` + TimezoneLocation *time.Location // Timezone in location format which the maintenance period is configured durationToStartFromMidnight time.Duration } @@ -85,6 +88,15 @@ func (c *Config) ValidateAndSetDefaults() error { if c.Duration <= 0 || c.Duration > 24*time.Hour { return errInvalidMaintenanceDuration } + if c.Timezone != "" { + c.TimezoneLocation, err = time.LoadLocation(c.Timezone) + if err != nil { + return fmt.Errorf("%w: %w", errInvalidTimezone, err) + } + } else { + c.Timezone = "UTC" + c.TimezoneLocation = time.UTC + } return nil } @@ -93,7 +105,10 @@ func (c Config) IsUnderMaintenance() bool { if !c.IsEnabled() { return false } - now := time.Now().UTC() + now := time.Now() + if c.TimezoneLocation != nil { + now = now.In(c.TimezoneLocation) + } var dayWhereMaintenancePeriodWouldStart time.Time if now.Hour() >= int(c.durationToStartFromMidnight.Hours()) { dayWhereMaintenancePeriodWouldStart = now.Truncate(24 * time.Hour) diff --git a/config/maintenance/maintenance_test.go b/config/maintenance/maintenance_test.go index 76ef9eb2..edbdad37 100644 --- a/config/maintenance/maintenance_test.go +++ b/config/maintenance/maintenance_test.go @@ -90,6 +90,15 @@ func TestConfig_ValidateAndSetDefaults(t *testing.T) { }, expectedError: errInvalidMaintenanceDuration, }, + { + name: "invalid-timezone", + cfg: &Config{ + Start: "23:00", + Duration: time.Hour, + Timezone: "invalid-timezone", + }, + expectedError: errInvalidTimezone, + }, { name: "every-day-at-2300", cfg: &Config{ @@ -126,6 +135,33 @@ func TestConfig_ValidateAndSetDefaults(t *testing.T) { }, expectedError: nil, }, + { + name: "timezone-amsterdam", + cfg: &Config{ + Start: "23:00", + Duration: time.Hour, + Timezone: "Europe/Amsterdam", + }, + expectedError: nil, + }, + { + name: "timezone-cet", + cfg: &Config{ + Start: "23:00", + Duration: time.Hour, + Timezone: "CET", + }, + expectedError: nil, + }, + { + name: "timezone-etc-plus-5", + cfg: &Config{ + Start: "23:00", + Duration: time.Hour, + Timezone: "Etc/GMT+5", + }, + expectedError: nil, + }, } for _, scenario := range scenarios { t.Run(scenario.name, func(t *testing.T) { @@ -220,7 +256,25 @@ func TestConfig_IsUnderMaintenance(t *testing.T) { expected: true, }, { - name: "under-maintenance-starting-4h-ago-for-3h", + name: "under-maintenance-amsterdam-timezone-starting-now-for-2h", + cfg: &Config{ + Start: fmt.Sprintf("%02d:00", now.Hour()), + Duration: 2 * time.Hour, + Timezone: "Europe/Amsterdam", + }, + expected: true, + }, + { + name: "under-maintenance-utc-timezone-starting-now-for-2h", + cfg: &Config{ + Start: fmt.Sprintf("%02d:00", now.Hour()), + Duration: 2 * time.Hour, + Timezone: "UTC", + }, + expected: true, + }, + { + name: "not-under-maintenance-starting-4h-ago-for-3h", cfg: &Config{ Start: fmt.Sprintf("%02d:00", normalizeHour(now.Hour()-4)), Duration: 3 * time.Hour, @@ -228,7 +282,7 @@ func TestConfig_IsUnderMaintenance(t *testing.T) { expected: false, }, { - name: "under-maintenance-starting-5h-ago-for-1h", + name: "not-under-maintenance-starting-5h-ago-for-1h", cfg: &Config{ Start: fmt.Sprintf("%02d:00", normalizeHour(now.Hour()-5)), Duration: time.Hour, @@ -253,6 +307,16 @@ func TestConfig_IsUnderMaintenance(t *testing.T) { }, expected: false, }, + { + name: "not-under-maintenance-los-angeles-timezone-starting-now-for-2h-today", + cfg: &Config{ + Start: fmt.Sprintf("%02d:00", now.Hour()), + Duration: 2 * time.Hour, + Timezone: "America/Los_Angeles", + Every: []string{now.Weekday().String()}, + }, + expected: false, + }, } for _, scenario := range scenarios { t.Run(scenario.name, func(t *testing.T) {