Skip to content

Commit af00dfd

Browse files
lefesTwiN
andauthored
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 <[email protected]>
1 parent b219139 commit af00dfd

File tree

3 files changed

+86
-6
lines changed

3 files changed

+86
-6
lines changed

README.md

+3-2
Original file line numberDiff line numberDiff line change
@@ -1459,22 +1459,23 @@ To do that, you'll have to use the maintenance configuration:
14591459
| `maintenance.enabled` | Whether the maintenance period is enabled | `true` |
14601460
| `maintenance.start` | Time at which the maintenance window starts in `hh:mm` format (e.g. `23:00`) | Required `""` |
14611461
| `maintenance.duration` | Duration of the maintenance window (e.g. `1h`, `30m`) | Required `""` |
1462+
| `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` |
14621463
| `maintenance.every` | Days on which the maintenance period applies (e.g. `[Monday, Thursday]`).<br />If left empty, the maintenance window applies every day | `[]` |
14631464

1464-
> 📝 The maintenance configuration uses UTC
1465-
14661465
Here's an example:
14671466
```yaml
14681467
maintenance:
14691468
start: 23:00
14701469
duration: 1h
1470+
timezone: "Europe/Amsterdam"
14711471
every: [Monday, Thursday]
14721472
```
14731473
Note that you can also specify each day on separate lines:
14741474
```yaml
14751475
maintenance:
14761476
start: 23:00
14771477
duration: 1h
1478+
timezone: "Europe/Amsterdam"
14781479
every:
14791480
- Monday
14801481
- Thursday

config/maintenance/maintenance.go

+17-2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ var (
1212
errInvalidMaintenanceStartFormat = errors.New("invalid maintenance start format: must be hh:mm, between 00:00 and 23:59 inclusively (e.g. 23:00)")
1313
errInvalidMaintenanceDuration = errors.New("invalid maintenance duration: must be bigger than 0 (e.g. 30m)")
1414
errInvalidDayName = fmt.Errorf("invalid value specified for 'on'. supported values are %s", longDayNames)
15+
errInvalidTimezone = errors.New("invalid timezone specified or format not supported. Use IANA timezone format (e.g. America/Sao_Paulo)")
1516

1617
longDayNames = []string{
1718
"Sunday",
@@ -27,17 +28,19 @@ var (
2728
// Config allows for the configuration of a maintenance period.
2829
// During this maintenance period, no alerts will be sent.
2930
//
30-
// Uses UTC.
31+
// Uses UTC by default.
3132
type Config struct {
3233
Enabled *bool `yaml:"enabled"` // Whether the maintenance period is enabled. Enabled by default if nil.
3334
Start string `yaml:"start"` // Time at which the maintenance period starts (e.g. 23:00)
3435
Duration time.Duration `yaml:"duration"` // Duration of the maintenance period (e.g. 4h)
36+
Timezone string `yaml:"timezone"` // Timezone in string format which the maintenance period is configured (e.g. America/Sao_Paulo)
3537

3638
// Every is a list of days of the week during which maintenance period applies.
3739
// See longDayNames for list of valid values.
3840
// Every day if empty.
3941
Every []string `yaml:"every"`
4042

43+
TimezoneLocation *time.Location // Timezone in location format which the maintenance period is configured
4144
durationToStartFromMidnight time.Duration
4245
}
4346

@@ -85,6 +88,15 @@ func (c *Config) ValidateAndSetDefaults() error {
8588
if c.Duration <= 0 || c.Duration > 24*time.Hour {
8689
return errInvalidMaintenanceDuration
8790
}
91+
if c.Timezone != "" {
92+
c.TimezoneLocation, err = time.LoadLocation(c.Timezone)
93+
if err != nil {
94+
return fmt.Errorf("%w: %w", errInvalidTimezone, err)
95+
}
96+
} else {
97+
c.Timezone = "UTC"
98+
c.TimezoneLocation = time.UTC
99+
}
88100
return nil
89101
}
90102

@@ -93,7 +105,10 @@ func (c Config) IsUnderMaintenance() bool {
93105
if !c.IsEnabled() {
94106
return false
95107
}
96-
now := time.Now().UTC()
108+
now := time.Now()
109+
if c.TimezoneLocation != nil {
110+
now = now.In(c.TimezoneLocation)
111+
}
97112
var dayWhereMaintenancePeriodWouldStart time.Time
98113
if now.Hour() >= int(c.durationToStartFromMidnight.Hours()) {
99114
dayWhereMaintenancePeriodWouldStart = now.Truncate(24 * time.Hour)

config/maintenance/maintenance_test.go

+66-2
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,15 @@ func TestConfig_ValidateAndSetDefaults(t *testing.T) {
9090
},
9191
expectedError: errInvalidMaintenanceDuration,
9292
},
93+
{
94+
name: "invalid-timezone",
95+
cfg: &Config{
96+
Start: "23:00",
97+
Duration: time.Hour,
98+
Timezone: "invalid-timezone",
99+
},
100+
expectedError: errInvalidTimezone,
101+
},
93102
{
94103
name: "every-day-at-2300",
95104
cfg: &Config{
@@ -126,6 +135,33 @@ func TestConfig_ValidateAndSetDefaults(t *testing.T) {
126135
},
127136
expectedError: nil,
128137
},
138+
{
139+
name: "timezone-amsterdam",
140+
cfg: &Config{
141+
Start: "23:00",
142+
Duration: time.Hour,
143+
Timezone: "Europe/Amsterdam",
144+
},
145+
expectedError: nil,
146+
},
147+
{
148+
name: "timezone-cet",
149+
cfg: &Config{
150+
Start: "23:00",
151+
Duration: time.Hour,
152+
Timezone: "CET",
153+
},
154+
expectedError: nil,
155+
},
156+
{
157+
name: "timezone-etc-plus-5",
158+
cfg: &Config{
159+
Start: "23:00",
160+
Duration: time.Hour,
161+
Timezone: "Etc/GMT+5",
162+
},
163+
expectedError: nil,
164+
},
129165
}
130166
for _, scenario := range scenarios {
131167
t.Run(scenario.name, func(t *testing.T) {
@@ -220,15 +256,33 @@ func TestConfig_IsUnderMaintenance(t *testing.T) {
220256
expected: true,
221257
},
222258
{
223-
name: "under-maintenance-starting-4h-ago-for-3h",
259+
name: "under-maintenance-amsterdam-timezone-starting-now-for-2h",
260+
cfg: &Config{
261+
Start: fmt.Sprintf("%02d:00", now.Hour()),
262+
Duration: 2 * time.Hour,
263+
Timezone: "Europe/Amsterdam",
264+
},
265+
expected: true,
266+
},
267+
{
268+
name: "under-maintenance-utc-timezone-starting-now-for-2h",
269+
cfg: &Config{
270+
Start: fmt.Sprintf("%02d:00", now.Hour()),
271+
Duration: 2 * time.Hour,
272+
Timezone: "UTC",
273+
},
274+
expected: true,
275+
},
276+
{
277+
name: "not-under-maintenance-starting-4h-ago-for-3h",
224278
cfg: &Config{
225279
Start: fmt.Sprintf("%02d:00", normalizeHour(now.Hour()-4)),
226280
Duration: 3 * time.Hour,
227281
},
228282
expected: false,
229283
},
230284
{
231-
name: "under-maintenance-starting-5h-ago-for-1h",
285+
name: "not-under-maintenance-starting-5h-ago-for-1h",
232286
cfg: &Config{
233287
Start: fmt.Sprintf("%02d:00", normalizeHour(now.Hour()-5)),
234288
Duration: time.Hour,
@@ -253,6 +307,16 @@ func TestConfig_IsUnderMaintenance(t *testing.T) {
253307
},
254308
expected: false,
255309
},
310+
{
311+
name: "not-under-maintenance-los-angeles-timezone-starting-now-for-2h-today",
312+
cfg: &Config{
313+
Start: fmt.Sprintf("%02d:00", now.Hour()),
314+
Duration: 2 * time.Hour,
315+
Timezone: "America/Los_Angeles",
316+
Every: []string{now.Weekday().String()},
317+
},
318+
expected: false,
319+
},
256320
}
257321
for _, scenario := range scenarios {
258322
t.Run(scenario.name, func(t *testing.T) {

0 commit comments

Comments
 (0)