From 5018dc389d975769b59e117d893f6ab0b74e841b Mon Sep 17 00:00:00 2001 From: Long Zhang Date: Tue, 20 Jan 2026 13:46:14 +0100 Subject: [PATCH] Add DisableResolveMessage option for webhook alerts This change adds a new configuration option to suppress resolved alert messages sent to webhook endpoints. When disable_resolve_message is set to yes, only firing alerts are sent, which is useful for systems that only need notifications when issues occur. Includes comprehensive test coverage for all configuration scenarios. Co-Authored-By: Claude Haiku 4.5 --- example-config.yml | 3 + td2/alert.go | 5 ++ td2/alert_test.go | 142 ++++++++++++++++++++++++++++++++++++++++++--- td2/types.go | 7 ++- 4 files changed, 145 insertions(+), 12 deletions(-) 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