diff --git a/example-config.yml b/example-config.yml index 4371962..f8749a4 100644 --- a/example-config.yml +++ b/example-config.yml @@ -75,6 +75,9 @@ default_alert_config: url: https://your-webhook-endpoint.example.com/alerts # Severity threshold defines the minimum severity level at which the alerts are sent to this channel severity_threshold: info + # When set to yes, resolved alert messages will not be sent to the webhook + # This is useful when you only want to be notified of firing alerts + disable_resolve_message: no # Alert defaults shared by all chains # If the chain stops seeing new blocks, should an alert be sent? diff --git a/td2/alert.go b/td2/alert.go index b3fd7ab..a9295cc 100644 --- a/td2/alert.go +++ b/td2/alert.go @@ -437,6 +437,11 @@ func notifyWebhook(msg *alertMsg) (err error) { return nil } + // Skip sending resolved messages if disabled in config + if msg.resolved && boolVal(msg.alertConfig.Webhook.DisableResolveMessage) { + return nil + } + status := "firing" if msg.resolved { status = "resolved" diff --git a/td2/alert_test.go b/td2/alert_test.go index 3f47bd2..1010036 100644 --- a/td2/alert_test.go +++ b/td2/alert_test.go @@ -462,10 +462,12 @@ func TestNotifySlack(t *testing.T) { func TestNotifyWebhook(t *testing.T) { tests := []struct { - name string - msg *alertMsg - serverResponse int - expectError bool + name string + msg *alertMsg + serverResponse int + expectError bool + expectServerCall bool + setupAlarms func(alarms *alarmCache) // optional setup for existing alarms }{ { name: "successful notification", @@ -481,8 +483,9 @@ func TestNotifyWebhook(t *testing.T) { Webhook: WebhookConfig{SeverityThreshold: "info"}, }, }, - serverResponse: 200, - expectError: false, + serverResponse: 200, + expectError: false, + expectServerCall: true, }, { name: "server error", @@ -498,15 +501,119 @@ func TestNotifyWebhook(t *testing.T) { Webhook: WebhookConfig{SeverityThreshold: "info"}, }, }, - serverResponse: 500, - expectError: true, + serverResponse: 500, + expectError: true, + expectServerCall: true, }, { name: "webhook disabled", msg: &alertMsg{ wh: false, }, - expectError: false, + expectError: false, + expectServerCall: false, + }, + { + name: "resolved message skipped when DisableResolveMessage is true", + msg: &alertMsg{ + wh: true, + chain: "test-chain", + message: "test resolved message", + severity: "critical", + uniqueId: "test_alert_resolved_skip", + resolved: true, + whURL: "", // will be set to test server URL + alertConfig: &AlertConfig{ + Webhook: WebhookConfig{ + SeverityThreshold: "info", + DisableResolveMessage: &[]bool{true}[0], + }, + }, + }, + serverResponse: 200, + expectError: false, + expectServerCall: false, + // Set up existing alert so the resolved message would normally be sent + setupAlarms: func(a *alarmCache) { + a.SentWHAlarms["test_alert_resolved_skip"] = alertMsgCache{ + Message: "Previous alert", + SentTime: time.Now().Add(-1 * time.Hour), + } + }, + }, + { + name: "resolved message sent when DisableResolveMessage is false", + msg: &alertMsg{ + wh: true, + chain: "test-chain", + message: "test resolved message", + severity: "critical", + uniqueId: "test_alert_resolved_send", + resolved: true, + whURL: "", // will be set to test server URL + alertConfig: &AlertConfig{ + Webhook: WebhookConfig{ + SeverityThreshold: "info", + DisableResolveMessage: &[]bool{false}[0], + }, + }, + }, + serverResponse: 200, + expectError: false, + expectServerCall: true, + // Set up existing alert so the resolved message can be sent + setupAlarms: func(a *alarmCache) { + a.SentWHAlarms["test_alert_resolved_send"] = alertMsgCache{ + Message: "Previous alert", + SentTime: time.Now().Add(-1 * time.Hour), + } + }, + }, + { + name: "resolved message sent when DisableResolveMessage is nil (default)", + msg: &alertMsg{ + wh: true, + chain: "test-chain", + message: "test resolved message", + severity: "critical", + uniqueId: "test_alert_resolved_nil", + resolved: true, + whURL: "", // will be set to test server URL + alertConfig: &AlertConfig{ + Webhook: WebhookConfig{SeverityThreshold: "info"}, + }, + }, + serverResponse: 200, + expectError: false, + expectServerCall: true, + // Set up existing alert so the resolved message can be sent + setupAlarms: func(a *alarmCache) { + a.SentWHAlarms["test_alert_resolved_nil"] = alertMsgCache{ + Message: "Previous alert", + SentTime: time.Now().Add(-1 * time.Hour), + } + }, + }, + { + name: "firing message sent even when DisableResolveMessage is true", + msg: &alertMsg{ + wh: true, + chain: "test-chain", + message: "test firing message", + severity: "critical", + uniqueId: "test_alert_firing", + resolved: false, + whURL: "", // will be set to test server URL + alertConfig: &AlertConfig{ + Webhook: WebhookConfig{ + SeverityThreshold: "info", + DisableResolveMessage: &[]bool{true}[0], + }, + }, + }, + serverResponse: 200, + expectError: false, + expectServerCall: true, }, } @@ -530,9 +637,18 @@ func TestNotifyWebhook(t *testing.T) { // Reset alarms for each test testAlarms.SentWHAlarms = make(map[string]alertMsgCache) + // Run optional alarm setup (e.g., to set up existing alerts for resolved message tests) + if tt.setupAlarms != nil { + tt.setupAlarms(testAlarms) + } + + // Track whether server was called + serverCalled := false + if tt.msg.wh { // Create test server server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + serverCalled = true w.WriteHeader(tt.serverResponse) })) defer server.Close() @@ -546,6 +662,14 @@ func TestNotifyWebhook(t *testing.T) { if !tt.expectError && err != nil { t.Errorf("Expected no error but got: %v", err) } + + // Verify server call expectation + if tt.expectServerCall && !serverCalled { + t.Errorf("Expected server to be called but it was not") + } + if !tt.expectServerCall && serverCalled { + t.Errorf("Expected server NOT to be called but it was") + } }) } } diff --git a/td2/types.go b/td2/types.go index 2f72bfd..6957852 100644 --- a/td2/types.go +++ b/td2/types.go @@ -359,9 +359,10 @@ type SlackConfig struct { // WebhookConfig holds the information needed to send alerts to a generic webhook endpoint // The payload follows a Grafana-like format for broad compatibility type WebhookConfig struct { - Enabled *bool `yaml:"enabled"` - URL string `yaml:"url"` - SeverityThreshold string `yaml:"severity_threshold"` + Enabled *bool `yaml:"enabled"` + URL string `yaml:"url"` + SeverityThreshold string `yaml:"severity_threshold"` + DisableResolveMessage *bool `yaml:"disable_resolve_message"` } // HealthcheckConfig holds the information needed to send pings to a healthcheck endpoint