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 <twin@linux.com>
This commit is contained in:
lefes 2024-07-02 02:41:33 +03:00 committed by GitHub
parent b2191391f6
commit af00dfdb73
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 86 additions and 6 deletions

View File

@ -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.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.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.duration` | Duration of the maintenance window (e.g. `1h`, `30m`) | Required `""` |
| `maintenance.timezone` | Timezone of the maintenance window format (e.g. `Europe/Amsterdam`).<br />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]`).<br />If left empty, the maintenance window applies every day | `[]` | | `maintenance.every` | Days on which the maintenance period applies (e.g. `[Monday, Thursday]`).<br />If left empty, the maintenance window applies every day | `[]` |
> 📝 The maintenance configuration uses UTC
Here's an example: Here's an example:
```yaml ```yaml
maintenance: maintenance:
start: 23:00 start: 23:00
duration: 1h duration: 1h
timezone: "Europe/Amsterdam"
every: [Monday, Thursday] every: [Monday, Thursday]
``` ```
Note that you can also specify each day on separate lines: 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: maintenance:
start: 23:00 start: 23:00
duration: 1h duration: 1h
timezone: "Europe/Amsterdam"
every: every:
- Monday - Monday
- Thursday - Thursday

View File

@ -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)") 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)") 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) 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{ longDayNames = []string{
"Sunday", "Sunday",
@ -27,17 +28,19 @@ var (
// Config allows for the configuration of a maintenance period. // Config allows for the configuration of a maintenance period.
// During this maintenance period, no alerts will be sent. // During this maintenance period, no alerts will be sent.
// //
// Uses UTC. // Uses UTC by default.
type Config struct { type Config struct {
Enabled *bool `yaml:"enabled"` // Whether the maintenance period is enabled. Enabled by default if nil. 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) 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) 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. // Every is a list of days of the week during which maintenance period applies.
// See longDayNames for list of valid values. // See longDayNames for list of valid values.
// Every day if empty. // Every day if empty.
Every []string `yaml:"every"` Every []string `yaml:"every"`
TimezoneLocation *time.Location // Timezone in location format which the maintenance period is configured
durationToStartFromMidnight time.Duration durationToStartFromMidnight time.Duration
} }
@ -85,6 +88,15 @@ func (c *Config) ValidateAndSetDefaults() error {
if c.Duration <= 0 || c.Duration > 24*time.Hour { if c.Duration <= 0 || c.Duration > 24*time.Hour {
return errInvalidMaintenanceDuration 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 return nil
} }
@ -93,7 +105,10 @@ func (c Config) IsUnderMaintenance() bool {
if !c.IsEnabled() { if !c.IsEnabled() {
return false return false
} }
now := time.Now().UTC() now := time.Now()
if c.TimezoneLocation != nil {
now = now.In(c.TimezoneLocation)
}
var dayWhereMaintenancePeriodWouldStart time.Time var dayWhereMaintenancePeriodWouldStart time.Time
if now.Hour() >= int(c.durationToStartFromMidnight.Hours()) { if now.Hour() >= int(c.durationToStartFromMidnight.Hours()) {
dayWhereMaintenancePeriodWouldStart = now.Truncate(24 * time.Hour) dayWhereMaintenancePeriodWouldStart = now.Truncate(24 * time.Hour)

View File

@ -90,6 +90,15 @@ func TestConfig_ValidateAndSetDefaults(t *testing.T) {
}, },
expectedError: errInvalidMaintenanceDuration, expectedError: errInvalidMaintenanceDuration,
}, },
{
name: "invalid-timezone",
cfg: &Config{
Start: "23:00",
Duration: time.Hour,
Timezone: "invalid-timezone",
},
expectedError: errInvalidTimezone,
},
{ {
name: "every-day-at-2300", name: "every-day-at-2300",
cfg: &Config{ cfg: &Config{
@ -126,6 +135,33 @@ func TestConfig_ValidateAndSetDefaults(t *testing.T) {
}, },
expectedError: nil, 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 { for _, scenario := range scenarios {
t.Run(scenario.name, func(t *testing.T) { t.Run(scenario.name, func(t *testing.T) {
@ -220,7 +256,25 @@ func TestConfig_IsUnderMaintenance(t *testing.T) {
expected: true, 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{ cfg: &Config{
Start: fmt.Sprintf("%02d:00", normalizeHour(now.Hour()-4)), Start: fmt.Sprintf("%02d:00", normalizeHour(now.Hour()-4)),
Duration: 3 * time.Hour, Duration: 3 * time.Hour,
@ -228,7 +282,7 @@ func TestConfig_IsUnderMaintenance(t *testing.T) {
expected: false, expected: false,
}, },
{ {
name: "under-maintenance-starting-5h-ago-for-1h", name: "not-under-maintenance-starting-5h-ago-for-1h",
cfg: &Config{ cfg: &Config{
Start: fmt.Sprintf("%02d:00", normalizeHour(now.Hour()-5)), Start: fmt.Sprintf("%02d:00", normalizeHour(now.Hour()-5)),
Duration: time.Hour, Duration: time.Hour,
@ -253,6 +307,16 @@ func TestConfig_IsUnderMaintenance(t *testing.T) {
}, },
expected: false, 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 { for _, scenario := range scenarios {
t.Run(scenario.name, func(t *testing.T) { t.Run(scenario.name, func(t *testing.T) {