Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 14 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1819,18 +1819,20 @@ endpoints:


#### Configuring Pushover alerts
| Parameter | Description | Default |
|:--------------------------------------|:-------------------------------------------------------------------------------------------------------------|:----------------------|
| `alerting.pushover` | Configuration for alerts of type `pushover` | `{}` |
| `alerting.pushover.application-token` | Pushover application token | `""` |
| `alerting.pushover.user-key` | User or group key | `""` |
| `alerting.pushover.title` | Fixed title for all messages sent via Pushover | `"Gatus: <endpoint>"` |
| `alerting.pushover.priority` | Priority of all messages, ranging from -2 (very low) to 2 (emergency) | `0` |
| `alerting.pushover.resolved-priority` | Override the priority of messages on resolved, ranging from -2 (very low) to 2 (emergency) | `0` |
| `alerting.pushover.sound` | Sound of all messages<br />See [sounds](https://pushover.net/api#sounds) for all valid choices. | `""` |
| `alerting.pushover.ttl` | Set the Time-to-live of the message to be automatically deleted from pushover notifications | `0` |
| `alerting.pushover.device` | Device to send the message to (optional)<br/>See [devices](https://pushover.net/api#identifiers) for details | `""` (all devices) |
| `alerting.pushover.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert) | N/A |
| Parameter | Description | Default |
|:--------------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------|:----------------------|
| `alerting.pushover` | Configuration for alerts of type `pushover` | `{}` |
| `alerting.pushover.application-token` | Pushover application token | `""` |
| `alerting.pushover.user-key` | User or group key | `""` |
| `alerting.pushover.title` | Fixed title for all messages sent via Pushover | `"Gatus: <endpoint>"` |
| `alerting.pushover.priority` | Priority of all messages, ranging from -2 (very low) to 2 (emergency) | `0` |
| `alerting.pushover.resolved-priority` | Override the priority of messages on resolved, ranging from -2 (very low) to 2 (emergency) | `0` |
| `alerting.pushover.sound` | Sound of all messages<br />See [sounds](https://pushover.net/api#sounds) for all valid choices. | `""` |
| `alerting.pushover.ttl` | Set the Time-to-live of the message to be automatically deleted from pushover notifications | `0` |
| `alerting.pushover.device` | Device to send the message to (optional)<br/>See [devices](https://pushover.net/api#identifiers) for details | `""` (all devices) |
| `alerting.pushover.retry` | How often (in seconds) to retry emergency-priority (priority 2) notifications (minimum: 30 seconds)<br/>See [priority](https://pushover.net/api#priority) | `60` |
| `alerting.pushover.expire` | How long (in seconds) to continue retrying emergency-priority (priority 2) notifications (maximum: 10800 seconds)<br/>See [priority](https://pushover.net/api#priority) | `3600` |
| `alerting.pushover.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert) | N/A |

```yaml
alerting:
Expand Down
61 changes: 59 additions & 2 deletions alerting/provider/pushover/pushover.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,18 @@ type Config struct {
// Device to send the message to (see: https://pushover.net/api#devices)
// default: "" (all devices)
Device string `yaml:"device,omitempty"`

// Retry parameter specifies how often (in seconds) the Pushover servers will retry the notification to the user
// Required when Priority is 2, otherwise ignored
// Minimum value is 30 seconds
// default: 60
Retry int `yaml:"retry,omitempty"`

// Expire parameter specifies how long (in seconds) the notification will continue to be retried
// Required when Priority is 2, otherwise ignored
// Maximum value is 10800 seconds (3 hours)
// default: 3600
Expire int `yaml:"expire,omitempty"`
}

func (cfg *Config) Validate() error {
Expand All @@ -79,6 +91,37 @@ func (cfg *Config) Validate() error {
if len(cfg.Device) > 25 {
return ErrInvalidDevice
}
// Set default values for retry and expire when priority is 2
if cfg.Priority == 2 {
if cfg.Retry == 0 {
cfg.Retry = 60 // default: 60 seconds
}
if cfg.Expire == 0 {
cfg.Expire = 3600 // default: 3600 seconds (1 hour)
}
// Validate retry and expire values
if cfg.Retry < 30 {
return errors.New("retry must be at least 30 seconds when priority is 2")
}
if cfg.Expire > 10800 {
return errors.New("expire must not exceed 10800 seconds (3 hours) when priority is 2")
}
}
if cfg.ResolvedPriority == 2 {
if cfg.Retry == 0 {
cfg.Retry = 60 // default: 60 seconds
}
if cfg.Expire == 0 {
cfg.Expire = 3600 // default: 3600 seconds (1 hour)
}
// Validate retry and expire values
if cfg.Retry < 30 {
return errors.New("retry must be at least 30 seconds when resolved-priority is 2")
}
if cfg.Expire > 10800 {
return errors.New("expire must not exceed 10800 seconds (3 hours) when resolved-priority is 2")
}
}
return nil
}

Expand Down Expand Up @@ -107,6 +150,12 @@ func (cfg *Config) Merge(override *Config) {
if len(override.Device) > 0 {
cfg.Device = override.Device
}
if override.Retry > 0 {
cfg.Retry = override.Retry
}
if override.Expire > 0 {
cfg.Expire = override.Expire
}
}

// AlertProvider is the configuration necessary for sending an alert using Pushover
Expand Down Expand Up @@ -157,6 +206,8 @@ type Body struct {
Sound string `json:"sound,omitempty"`
TTL int `json:"ttl,omitempty"`
Device string `json:"device,omitempty"`
Retry int `json:"retry,omitempty"`
Expire int `json:"expire,omitempty"`
}

// buildRequestBody builds the request body for the provider
Expand Down Expand Up @@ -186,7 +237,7 @@ func (provider *AlertProvider) buildRequestBody(cfg *Config, ep *endpoint.Endpoi
if cfg.Title != "" {
title = cfg.Title
}
body, _ := json.Marshal(Body{
bodyStruct := Body{
Token: cfg.ApplicationToken,
User: cfg.UserKey,
Title: title,
Expand All @@ -196,7 +247,13 @@ func (provider *AlertProvider) buildRequestBody(cfg *Config, ep *endpoint.Endpoi
Sound: cfg.Sound,
TTL: cfg.TTL,
Device: cfg.Device,
})
}
// Include retry and expire when priority is 2 (Emergency)
if priority == 2 {
bodyStruct.Retry = cfg.Retry
bodyStruct.Expire = cfg.Expire
}
body, _ := json.Marshal(bodyStruct)
return body
}

Expand Down
70 changes: 61 additions & 9 deletions alerting/provider/pushover/pushover_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,51 @@ func TestPushoverAlertProvider_IsValid(t *testing.T) {
t.Error("provider should've been invalid")
}
})
t.Run("priority-2-with-defaults", func(t *testing.T) {
provider := AlertProvider{
DefaultConfig: Config{
ApplicationToken: "aTokenWithLengthOf30characters",
UserKey: "aTokenWithLengthOf30characters",
Priority: 2,
},
}
if err := provider.Validate(); err != nil {
t.Error("provider should've been valid, got error:", err)
}
// Check that defaults were set
if provider.DefaultConfig.Retry != 60 {
t.Errorf("expected retry to be 60, got %d", provider.DefaultConfig.Retry)
}
if provider.DefaultConfig.Expire != 3600 {
t.Errorf("expected expire to be 3600, got %d", provider.DefaultConfig.Expire)
}
})
t.Run("priority-2-with-invalid-retry", func(t *testing.T) {
provider := AlertProvider{
DefaultConfig: Config{
ApplicationToken: "aTokenWithLengthOf30characters",
UserKey: "aTokenWithLengthOf30characters",
Priority: 2,
Retry: 10, // less than minimum of 30
},
}
if err := provider.Validate(); err == nil {
t.Error("provider should've been invalid due to retry < 30")
}
})
t.Run("priority-2-with-invalid-expire", func(t *testing.T) {
provider := AlertProvider{
DefaultConfig: Config{
ApplicationToken: "aTokenWithLengthOf30characters",
UserKey: "aTokenWithLengthOf30characters",
Priority: 2,
Expire: 20000, // more than maximum of 10800
},
}
if err := provider.Validate(); err == nil {
t.Error("provider should've been invalid due to expire > 10800")
}
})
}

func TestAlertProvider_Send(t *testing.T) {
Expand Down Expand Up @@ -150,39 +195,46 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
},
{
Name: "resolved",
Provider: AlertProvider{DefaultConfig: Config{ApplicationToken: "TokenWithLengthOf30Characters2", UserKey: "TokenWithLengthOf30Characters5", Priority: 2, ResolvedPriority: 2}},
Provider: AlertProvider{DefaultConfig: Config{ApplicationToken: "TokenWithLengthOf30Characters2", UserKey: "TokenWithLengthOf30Characters5", Priority: 2, ResolvedPriority: 2, Retry: 60, Expire: 3600}},
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: true,
ExpectedBody: "{\"token\":\"TokenWithLengthOf30Characters2\",\"user\":\"TokenWithLengthOf30Characters5\",\"title\":\"Gatus: endpoint-name\",\"message\":\"An alert for \\u003cb\\u003eendpoint-name\\u003c/b\\u003e has been resolved after passing successfully 5 time(s) in a row with the following description: description-2\\n✅ - [CONNECTED] == true\\n✅ - [STATUS] == 200\",\"priority\":2,\"html\":1}",
ExpectedBody: "{\"token\":\"TokenWithLengthOf30Characters2\",\"user\":\"TokenWithLengthOf30Characters5\",\"title\":\"Gatus: endpoint-name\",\"message\":\"An alert for \\u003cb\\u003eendpoint-name\\u003c/b\\u003e has been resolved after passing successfully 5 time(s) in a row with the following description: description-2\\n✅ - [CONNECTED] == true\\n✅ - [STATUS] == 200\",\"priority\":2,\"html\":1,\"retry\":60,\"expire\":3600}",
},
{
Name: "resolved-priority",
Provider: AlertProvider{DefaultConfig: Config{ApplicationToken: "TokenWithLengthOf30Characters2", UserKey: "TokenWithLengthOf30Characters5", Title: "Gatus Notifications", Priority: 2, ResolvedPriority: 0}},
Provider: AlertProvider{DefaultConfig: Config{ApplicationToken: "TokenWithLengthOf30Characters2", UserKey: "TokenWithLengthOf30Characters5", Title: "Gatus Notifications", Priority: 2, ResolvedPriority: 0, Retry: 60, Expire: 3600}},
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: true,
ExpectedBody: "{\"token\":\"TokenWithLengthOf30Characters2\",\"user\":\"TokenWithLengthOf30Characters5\",\"title\":\"Gatus Notifications\",\"message\":\"An alert for \\u003cb\\u003eendpoint-name\\u003c/b\\u003e has been resolved after passing successfully 5 time(s) in a row with the following description: description-2\\n✅ - [CONNECTED] == true\\n✅ - [STATUS] == 200\",\"priority\":0,\"html\":1}",
},
{
Name: "with-sound",
Provider: AlertProvider{DefaultConfig: Config{ApplicationToken: "TokenWithLengthOf30Characters2", UserKey: "TokenWithLengthOf30Characters5", Title: "Gatus Notifications", Priority: 2, ResolvedPriority: 2, Sound: "falling"}},
Provider: AlertProvider{DefaultConfig: Config{ApplicationToken: "TokenWithLengthOf30Characters2", UserKey: "TokenWithLengthOf30Characters5", Title: "Gatus Notifications", Priority: 2, ResolvedPriority: 2, Sound: "falling", Retry: 60, Expire: 3600}},
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: true,
ExpectedBody: "{\"token\":\"TokenWithLengthOf30Characters2\",\"user\":\"TokenWithLengthOf30Characters5\",\"title\":\"Gatus Notifications\",\"message\":\"An alert for \\u003cb\\u003eendpoint-name\\u003c/b\\u003e has been resolved after passing successfully 5 time(s) in a row with the following description: description-2\\n✅ - [CONNECTED] == true\\n✅ - [STATUS] == 200\",\"priority\":2,\"html\":1,\"sound\":\"falling\"}",
ExpectedBody: "{\"token\":\"TokenWithLengthOf30Characters2\",\"user\":\"TokenWithLengthOf30Characters5\",\"title\":\"Gatus Notifications\",\"message\":\"An alert for \\u003cb\\u003eendpoint-name\\u003c/b\\u003e has been resolved after passing successfully 5 time(s) in a row with the following description: description-2\\n✅ - [CONNECTED] == true\\n✅ - [STATUS] == 200\",\"priority\":2,\"html\":1,\"sound\":\"falling\",\"retry\":60,\"expire\":3600}",
},
{
Name: "with-ttl",
Provider: AlertProvider{DefaultConfig: Config{ApplicationToken: "TokenWithLengthOf30Characters2", UserKey: "TokenWithLengthOf30Characters5", Title: "Gatus Notifications", Priority: 2, ResolvedPriority: 2, TTL: 3600}},
Provider: AlertProvider{DefaultConfig: Config{ApplicationToken: "TokenWithLengthOf30Characters2", UserKey: "TokenWithLengthOf30Characters5", Title: "Gatus Notifications", Priority: 2, ResolvedPriority: 2, TTL: 3600, Retry: 60, Expire: 3600}},
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: true,
ExpectedBody: "{\"token\":\"TokenWithLengthOf30Characters2\",\"user\":\"TokenWithLengthOf30Characters5\",\"title\":\"Gatus Notifications\",\"message\":\"An alert for \\u003cb\\u003eendpoint-name\\u003c/b\\u003e has been resolved after passing successfully 5 time(s) in a row with the following description: description-2\\n✅ - [CONNECTED] == true\\n✅ - [STATUS] == 200\",\"priority\":2,\"html\":1,\"ttl\":3600}",
ExpectedBody: "{\"token\":\"TokenWithLengthOf30Characters2\",\"user\":\"TokenWithLengthOf30Characters5\",\"title\":\"Gatus Notifications\",\"message\":\"An alert for \\u003cb\\u003eendpoint-name\\u003c/b\\u003e has been resolved after passing successfully 5 time(s) in a row with the following description: description-2\\n✅ - [CONNECTED] == true\\n✅ - [STATUS] == 200\",\"priority\":2,\"html\":1,\"ttl\":3600,\"retry\":60,\"expire\":3600}",
},
{
Name: "with-device",
Provider: AlertProvider{DefaultConfig: Config{ApplicationToken: "TokenWithLengthOf30Characters2", UserKey: "TokenWithLengthOf30Characters5", Title: "Gatus Notifications", Priority: 2, ResolvedPriority: 2, TTL: 3600, Device: "iphone15pro",}},
Provider: AlertProvider{DefaultConfig: Config{ApplicationToken: "TokenWithLengthOf30Characters2", UserKey: "TokenWithLengthOf30Characters5", Title: "Gatus Notifications", Priority: 2, ResolvedPriority: 2, TTL: 3600, Device: "iphone15pro", Retry: 60, Expire: 3600}},
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: true,
ExpectedBody: "{\"token\":\"TokenWithLengthOf30Characters2\",\"user\":\"TokenWithLengthOf30Characters5\",\"title\":\"Gatus Notifications\",\"message\":\"An alert for \\u003cb\\u003eendpoint-name\\u003c/b\\u003e has been resolved after passing successfully 5 time(s) in a row with the following description: description-2\\n✅ - [CONNECTED] == true\\n✅ - [STATUS] == 200\",\"priority\":2,\"html\":1,\"ttl\":3600,\"device\":\"iphone15pro\"}",
ExpectedBody: "{\"token\":\"TokenWithLengthOf30Characters2\",\"user\":\"TokenWithLengthOf30Characters5\",\"title\":\"Gatus Notifications\",\"message\":\"An alert for \\u003cb\\u003eendpoint-name\\u003c/b\\u003e has been resolved after passing successfully 5 time(s) in a row with the following description: description-2\\n✅ - [CONNECTED] == true\\n✅ - [STATUS] == 200\",\"priority\":2,\"html\":1,\"ttl\":3600,\"device\":\"iphone15pro\",\"retry\":60,\"expire\":3600}",
},
{
Name: "priority-2-with-retry-expire",
Provider: AlertProvider{DefaultConfig: Config{ApplicationToken: "TokenWithLengthOf30Characters1", UserKey: "TokenWithLengthOf30Characters4", Priority: 2, Retry: 60, Expire: 3600}},
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: false,
ExpectedBody: "{\"token\":\"TokenWithLengthOf30Characters1\",\"user\":\"TokenWithLengthOf30Characters4\",\"title\":\"Gatus: endpoint-name\",\"message\":\"An alert for \\u003cb\\u003eendpoint-name\\u003c/b\\u003e has been triggered due to having failed 3 time(s) in a row with the following description: description-1\\n❌ - [CONNECTED] == true\\n❌ - [STATUS] == 200\",\"priority\":2,\"html\":1,\"retry\":60,\"expire\":3600}",
},
}
for _, scenario := range scenarios {
t.Run(scenario.Name, func(t *testing.T) {
Expand Down