feat(alerting): Persist triggered alerts across application restart (#764)
* feat(alerting): Persist triggered alerts across application restart Fixes #679 * test(alerting): Add numerous tests related to alerts
This commit is contained in:
@ -1,9 +1,11 @@
|
||||
package sql
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||
"github.com/TwiN/gatus/v5/storage/store/common"
|
||||
"github.com/TwiN/gatus/v5/storage/store/common/paging"
|
||||
@ -81,13 +83,13 @@ var (
|
||||
)
|
||||
|
||||
func TestNewStore(t *testing.T) {
|
||||
if _, err := NewStore("", "TestNewStore.db", false); err != ErrDatabaseDriverNotSpecified {
|
||||
if _, err := NewStore("", t.TempDir()+"/TestNewStore.db", false); !errors.Is(err, ErrDatabaseDriverNotSpecified) {
|
||||
t.Error("expected error due to blank driver parameter")
|
||||
}
|
||||
if _, err := NewStore("sqlite", "", false); err != ErrPathNotSpecified {
|
||||
if _, err := NewStore("sqlite", "", false); !errors.Is(err, ErrPathNotSpecified) {
|
||||
t.Error("expected error due to blank path parameter")
|
||||
}
|
||||
if store, err := NewStore("sqlite", t.TempDir()+"/TestNewStore.db", false); err != nil {
|
||||
if store, err := NewStore("sqlite", t.TempDir()+"/TestNewStore.db", true); err != nil {
|
||||
t.Error("shouldn't have returned any error, got", err.Error())
|
||||
} else {
|
||||
_ = store.db.Close()
|
||||
@ -168,6 +170,40 @@ func TestStore_InsertCleansUpEventsAndResultsProperly(t *testing.T) {
|
||||
store.Clear()
|
||||
}
|
||||
|
||||
func TestStore_InsertWithCaching(t *testing.T) {
|
||||
store, _ := NewStore("sqlite", t.TempDir()+"/TestStore_InsertWithCaching.db", true)
|
||||
defer store.Close()
|
||||
// Add 2 results
|
||||
store.Insert(&testEndpoint, &testSuccessfulResult)
|
||||
store.Insert(&testEndpoint, &testSuccessfulResult)
|
||||
// Verify that they exist
|
||||
endpointStatuses, _ := store.GetAllEndpointStatuses(paging.NewEndpointStatusParams().WithResults(1, 20))
|
||||
if numberOfEndpointStatuses := len(endpointStatuses); numberOfEndpointStatuses != 1 {
|
||||
t.Fatalf("expected 1 EndpointStatus, got %d", numberOfEndpointStatuses)
|
||||
}
|
||||
if len(endpointStatuses[0].Results) != 2 {
|
||||
t.Fatalf("expected 2 results, got %d", len(endpointStatuses[0].Results))
|
||||
}
|
||||
// Add 2 more results
|
||||
store.Insert(&testEndpoint, &testUnsuccessfulResult)
|
||||
store.Insert(&testEndpoint, &testUnsuccessfulResult)
|
||||
// Verify that they exist
|
||||
endpointStatuses, _ = store.GetAllEndpointStatuses(paging.NewEndpointStatusParams().WithResults(1, 20))
|
||||
if numberOfEndpointStatuses := len(endpointStatuses); numberOfEndpointStatuses != 1 {
|
||||
t.Fatalf("expected 1 EndpointStatus, got %d", numberOfEndpointStatuses)
|
||||
}
|
||||
if len(endpointStatuses[0].Results) != 4 {
|
||||
t.Fatalf("expected 4 results, got %d", len(endpointStatuses[0].Results))
|
||||
}
|
||||
// Clear the store, which should also clear the cache
|
||||
store.Clear()
|
||||
// Verify that they no longer exist
|
||||
endpointStatuses, _ = store.GetAllEndpointStatuses(paging.NewEndpointStatusParams().WithResults(1, 20))
|
||||
if numberOfEndpointStatuses := len(endpointStatuses); numberOfEndpointStatuses != 0 {
|
||||
t.Fatalf("expected 0 EndpointStatus, got %d", numberOfEndpointStatuses)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStore_Persistence(t *testing.T) {
|
||||
path := t.TempDir() + "/TestStore_Persistence.db"
|
||||
store, _ := NewStore("sqlite", path, false)
|
||||
@ -368,10 +404,10 @@ func TestStore_NoRows(t *testing.T) {
|
||||
defer store.Close()
|
||||
tx, _ := store.db.Begin()
|
||||
defer tx.Rollback()
|
||||
if _, err := store.getLastEndpointResultSuccessValue(tx, 1); err != errNoRowsReturned {
|
||||
if _, err := store.getLastEndpointResultSuccessValue(tx, 1); !errors.Is(err, errNoRowsReturned) {
|
||||
t.Errorf("should've %v, got %v", errNoRowsReturned, err)
|
||||
}
|
||||
if _, err := store.getAgeOfOldestEndpointUptimeEntry(tx, 1); err != errNoRowsReturned {
|
||||
if _, err := store.getAgeOfOldestEndpointUptimeEntry(tx, 1); !errors.Is(err, errNoRowsReturned) {
|
||||
t.Errorf("should've %v, got %v", errNoRowsReturned, err)
|
||||
}
|
||||
}
|
||||
@ -564,3 +600,131 @@ func TestCacheKey(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTriggeredEndpointAlertsPersistence(t *testing.T) {
|
||||
store, _ := NewStore("sqlite", t.TempDir()+"/TestTriggeredEndpointAlertsPersistence.db", false)
|
||||
defer store.Close()
|
||||
yes, desc := false, "description"
|
||||
ep := testEndpoint
|
||||
ep.NumberOfSuccessesInARow = 0
|
||||
alrt := &alert.Alert{
|
||||
Type: alert.TypePagerDuty,
|
||||
Enabled: &yes,
|
||||
FailureThreshold: 4,
|
||||
SuccessThreshold: 2,
|
||||
Description: &desc,
|
||||
SendOnResolved: &yes,
|
||||
Triggered: true,
|
||||
ResolveKey: "1234567",
|
||||
}
|
||||
// Alert just triggered, so NumberOfSuccessesInARow is 0
|
||||
if err := store.UpsertTriggeredEndpointAlert(&ep, alrt); err != nil {
|
||||
t.Fatal("expected no error, got", err.Error())
|
||||
}
|
||||
exists, resolveKey, numberOfSuccessesInARow, err := store.GetTriggeredEndpointAlert(&ep, alrt)
|
||||
if err != nil {
|
||||
t.Fatal("expected no error, got", err.Error())
|
||||
}
|
||||
if !exists {
|
||||
t.Error("expected triggered alert to exist")
|
||||
}
|
||||
if resolveKey != alrt.ResolveKey {
|
||||
t.Errorf("expected resolveKey %s, got %s", alrt.ResolveKey, resolveKey)
|
||||
}
|
||||
if numberOfSuccessesInARow != ep.NumberOfSuccessesInARow {
|
||||
t.Errorf("expected persisted NumberOfSuccessesInARow to be %d, got %d", ep.NumberOfSuccessesInARow, numberOfSuccessesInARow)
|
||||
}
|
||||
// Endpoint just had a successful evaluation, so NumberOfSuccessesInARow is now 1
|
||||
ep.NumberOfSuccessesInARow++
|
||||
if err := store.UpsertTriggeredEndpointAlert(&ep, alrt); err != nil {
|
||||
t.Fatal("expected no error, got", err.Error())
|
||||
}
|
||||
exists, resolveKey, numberOfSuccessesInARow, err = store.GetTriggeredEndpointAlert(&ep, alrt)
|
||||
if err != nil {
|
||||
t.Error("expected no error, got", err.Error())
|
||||
}
|
||||
if !exists {
|
||||
t.Error("expected triggered alert to exist")
|
||||
}
|
||||
if resolveKey != alrt.ResolveKey {
|
||||
t.Errorf("expected resolveKey %s, got %s", alrt.ResolveKey, resolveKey)
|
||||
}
|
||||
if numberOfSuccessesInARow != ep.NumberOfSuccessesInARow {
|
||||
t.Errorf("expected persisted NumberOfSuccessesInARow to be %d, got %d", ep.NumberOfSuccessesInARow, numberOfSuccessesInARow)
|
||||
}
|
||||
// Simulate the endpoint having another successful evaluation, which means the alert is now resolved,
|
||||
// and we should delete the triggered alert from the store
|
||||
ep.NumberOfSuccessesInARow++
|
||||
if err := store.DeleteTriggeredEndpointAlert(&ep, alrt); err != nil {
|
||||
t.Fatal("expected no error, got", err.Error())
|
||||
}
|
||||
exists, _, _, err = store.GetTriggeredEndpointAlert(&ep, alrt)
|
||||
if err != nil {
|
||||
t.Error("expected no error, got", err.Error())
|
||||
}
|
||||
if exists {
|
||||
t.Error("expected triggered alert to no longer exist as it has been deleted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStore_DeleteAllTriggeredAlertsNotInChecksumsByEndpoint(t *testing.T) {
|
||||
store, _ := NewStore("sqlite", t.TempDir()+"/TestStore_DeleteAllTriggeredAlertsNotInChecksumsByEndpoint.db", false)
|
||||
defer store.Close()
|
||||
yes, desc := false, "description"
|
||||
ep1 := testEndpoint
|
||||
ep1.Name = "ep1"
|
||||
ep2 := testEndpoint
|
||||
ep2.Name = "ep2"
|
||||
alert1 := alert.Alert{
|
||||
Type: alert.TypePagerDuty,
|
||||
Enabled: &yes,
|
||||
FailureThreshold: 4,
|
||||
SuccessThreshold: 2,
|
||||
Description: &desc,
|
||||
SendOnResolved: &yes,
|
||||
Triggered: true,
|
||||
ResolveKey: "1234567",
|
||||
}
|
||||
alert2 := alert1
|
||||
alert2.Type, alert2.ResolveKey = alert.TypeSlack, ""
|
||||
alert3 := alert2
|
||||
if err := store.UpsertTriggeredEndpointAlert(&ep1, &alert1); err != nil {
|
||||
t.Fatal("expected no error, got", err.Error())
|
||||
}
|
||||
if err := store.UpsertTriggeredEndpointAlert(&ep1, &alert2); err != nil {
|
||||
t.Fatal("expected no error, got", err.Error())
|
||||
}
|
||||
if err := store.UpsertTriggeredEndpointAlert(&ep2, &alert3); err != nil {
|
||||
t.Fatal("expected no error, got", err.Error())
|
||||
}
|
||||
if exists, _, _, _ := store.GetTriggeredEndpointAlert(&ep1, &alert1); !exists {
|
||||
t.Error("expected alert1 to have been deleted")
|
||||
}
|
||||
if exists, _, _, _ := store.GetTriggeredEndpointAlert(&ep1, &alert2); !exists {
|
||||
t.Error("expected alert2 to exist for ep1")
|
||||
}
|
||||
if exists, _, _, _ := store.GetTriggeredEndpointAlert(&ep2, &alert3); !exists {
|
||||
t.Error("expected alert3 to exist for ep2")
|
||||
}
|
||||
// Now we simulate the alert configuration being updated, and the alert being resolved
|
||||
if deleted := store.DeleteAllTriggeredAlertsNotInChecksumsByEndpoint(&ep1, []string{alert2.Checksum()}); deleted != 1 {
|
||||
t.Errorf("expected 1 triggered alert to be deleted, got %d", deleted)
|
||||
}
|
||||
if exists, _, _, _ := store.GetTriggeredEndpointAlert(&ep1, &alert1); exists {
|
||||
t.Error("expected alert1 to have been deleted")
|
||||
}
|
||||
if exists, _, _, _ := store.GetTriggeredEndpointAlert(&ep1, &alert2); !exists {
|
||||
t.Error("expected alert2 to exist for ep1")
|
||||
}
|
||||
if exists, _, _, _ := store.GetTriggeredEndpointAlert(&ep2, &alert3); !exists {
|
||||
t.Error("expected alert3 to exist for ep2")
|
||||
}
|
||||
// Now let's just assume all alerts for ep1 were removed
|
||||
if deleted := store.DeleteAllTriggeredAlertsNotInChecksumsByEndpoint(&ep1, []string{}); deleted != 1 {
|
||||
t.Errorf("expected 1 triggered alert to be deleted, got %d", deleted)
|
||||
}
|
||||
// Make sure the alert for ep2 still exists
|
||||
if exists, _, _, _ := store.GetTriggeredEndpointAlert(&ep2, &alert3); !exists {
|
||||
t.Error("expected alert3 to exist for ep2")
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user