Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

r/burn_alert: Add support for budget rate burn alerts #391

Merged
merged 11 commits into from
Nov 15, 2023
Merged
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
31 changes: 25 additions & 6 deletions client/burn_alert.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,31 @@ type SLORef struct {
}

type BurnAlert struct {
ID string `json:"id,omitempty"`
ExhaustionMinutes int `json:"exhaustion_minutes"`
SLO SLORef `json:"slo"`
CreatedAt time.Time `json:"created_at,omitempty"`
UpdatedAt time.Time `json:"updated_at,omitempty"`
Recipients []NotificationRecipient `json:"recipients,omitempty"`
ID string `json:"id,omitempty"`
AlertType string `json:"alert_type"`
ExhaustionMinutes *int `json:"exhaustion_minutes,omitempty"`
BudgetRateWindowMinutes *int `json:"budget_rate_window_minutes,omitempty"`
BudgetRateDecreaseThresholdPerMillion *int `json:"budget_rate_decrease_threshold_per_million,omitempty"`
SLO SLORef `json:"slo"`
CreatedAt time.Time `json:"created_at,omitempty"`
UpdatedAt time.Time `json:"updated_at,omitempty"`
Recipients []NotificationRecipient `json:"recipients,omitempty"`
}

// BurnAlertAlertType represents a burn alert alert type
type BurnAlertAlertType string

const (
BurnAlertAlertTypeExhaustionTime BurnAlertAlertType = "exhaustion_time"
BurnAlertAlertTypeBudgetRate BurnAlertAlertType = "budget_rate"
)

// BurnAlertAlertTypes returns a list of valid burn alert alert types
func BurnAlertAlertTypes() []BurnAlertAlertType {
return []BurnAlertAlertType{
BurnAlertAlertTypeExhaustionTime,
BurnAlertAlertTypeBudgetRate,
}
}

func (s *burnalerts) ListForSLO(ctx context.Context, dataset string, sloId string) ([]BurnAlert, error) {
Expand Down
239 changes: 182 additions & 57 deletions client/burn_alert_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package client

import (
"context"
"fmt"
"testing"

"github.com/stretchr/testify/assert"
Expand All @@ -10,7 +11,6 @@ import (
func TestBurnAlerts(t *testing.T) {
ctx := context.Background()

var burnAlert *BurnAlert
var err error

c := newTestClient(t)
Expand Down Expand Up @@ -39,69 +39,194 @@ func TestBurnAlerts(t *testing.T) {
c.DerivedColumns.Delete(ctx, dataset, sli.ID)
})

t.Run("Create", func(t *testing.T) {
data := &BurnAlert{
ExhaustionMinutes: int(24 * 60), // 24 hours
SLO: SLORef{ID: slo.ID},
Recipients: []NotificationRecipient{
{
Type: "email",
Target: "[email protected]",
},
var defaultBurnAlert *BurnAlert
exhaustionMinutes24Hours := 24 * 60
defaultBurnAlertCreateRequest := BurnAlert{
ExhaustionMinutes: &exhaustionMinutes24Hours,
SLO: SLORef{ID: slo.ID},
Recipients: []NotificationRecipient{
{
Type: "email",
Target: "[email protected]",
},
}

burnAlert, err = c.BurnAlerts.Create(ctx, dataset, data)

assert.NoError(t, err, "failed to create BurnAlert")
assert.NotNil(t, burnAlert.ID, "BurnAlert ID is empty")
assert.NotNil(t, burnAlert.CreatedAt, "created at is empty")
assert.NotNil(t, burnAlert.UpdatedAt, "updated at is empty")
// copy dynamic fields before asserting equality
data.ID = burnAlert.ID
data.CreatedAt = burnAlert.CreatedAt
data.UpdatedAt = burnAlert.UpdatedAt
data.Recipients[0].ID = burnAlert.Recipients[0].ID
assert.Equal(t, data, burnAlert)
})

t.Run("Get", func(t *testing.T) {
getBA, err := c.BurnAlerts.Get(ctx, dataset, burnAlert.ID)
assert.NoError(t, err, "failed to get BurnAlert by ID")
assert.Equal(t, burnAlert, getBA)
})

t.Run("Update", func(t *testing.T) {
burnAlert.ExhaustionMinutes = int(4 * 60) // 4 hours
},
}
exhaustionMinutes1Hour := 60
defaultBurnAlertUpdateRequest := BurnAlert{
ExhaustionMinutes: &exhaustionMinutes1Hour,
SLO: SLORef{ID: slo.ID},
Recipients: []NotificationRecipient{
{
Type: "email",
Target: "[email protected]",
},
},
}

result, err := c.BurnAlerts.Update(ctx, dataset, burnAlert)
var exhaustionTimeBurnAlert *BurnAlert
exhaustionMinutes0Minutes := 0
exhaustionTimeBurnAlertCreateRequest := BurnAlert{
AlertType: string(BurnAlertAlertTypeExhaustionTime),
ExhaustionMinutes: &exhaustionMinutes0Minutes,
SLO: SLORef{ID: slo.ID},
Recipients: []NotificationRecipient{
{
Type: "email",
Target: "[email protected]",
},
},
}
exhaustionMinutes4Hours := 4 * 60
exhaustionTimeBurnAlertUpdateRequest := BurnAlert{
AlertType: string(BurnAlertAlertTypeExhaustionTime),
ExhaustionMinutes: &exhaustionMinutes4Hours,
SLO: SLORef{ID: slo.ID},
Recipients: []NotificationRecipient{
{
Type: "email",
Target: "[email protected]",
},
},
}

assert.NoError(t, err, "failed to update BurnAlert")
// copy dynamic field before asserting equality
burnAlert.UpdatedAt = result.UpdatedAt
assert.Equal(t, burnAlert, result)
})
var budgetRateBurnAlert *BurnAlert
budgetRateWindowMinutes1Hour := 60
budgetRateDecreaseThresholdPerMillion1Percent := 10000
budgetRateBurnAlertCreateRequest := BurnAlert{
AlertType: string(BurnAlertAlertTypeBudgetRate),
BudgetRateWindowMinutes: &budgetRateWindowMinutes1Hour,
BudgetRateDecreaseThresholdPerMillion: &budgetRateDecreaseThresholdPerMillion1Percent,
SLO: SLORef{ID: slo.ID},
Recipients: []NotificationRecipient{
{
Type: "email",
Target: "[email protected]",
},
},
}
budgetRateWindowMinutes2Hours := 2 * 60
budgetRateDecreaseThresholdPerMillion5Percent := 10000
budgetRateBurnAlertUpdateRequest := BurnAlert{
AlertType: string(BurnAlertAlertTypeBudgetRate),
BudgetRateWindowMinutes: &budgetRateWindowMinutes2Hours,
BudgetRateDecreaseThresholdPerMillion: &budgetRateDecreaseThresholdPerMillion5Percent,
SLO: SLORef{ID: slo.ID},
Recipients: []NotificationRecipient{
{
Type: "email",
Target: "[email protected]",
},
},
}

t.Run("ListForSLO", func(t *testing.T) {
results, err := c.BurnAlerts.ListForSLO(ctx, dataset, slo.ID)
burnAlert.Recipients = []NotificationRecipient{}
assert.NoError(t, err, "failed to list burn alerts for SLO")
assert.NotZero(t, len(results))
assert.Equal(t, burnAlert.ID, results[0].ID, "newly created BurnAlert not in list of SLO's burn alerts")
})
testCases := map[string]struct {
alertType string
createRequest BurnAlert
updateRequest BurnAlert
burnAlert *BurnAlert
}{
"default - exhaustion_time": {
alertType: string(BurnAlertAlertTypeExhaustionTime),
createRequest: defaultBurnAlertCreateRequest,
updateRequest: defaultBurnAlertUpdateRequest,
burnAlert: defaultBurnAlert,
},
"exhaustion_time": {
alertType: string(BurnAlertAlertTypeExhaustionTime),
createRequest: exhaustionTimeBurnAlertCreateRequest,
updateRequest: exhaustionTimeBurnAlertUpdateRequest,
burnAlert: exhaustionTimeBurnAlert,
},
"budget_rate": {
alertType: string(BurnAlertAlertTypeBudgetRate),
createRequest: budgetRateBurnAlertCreateRequest,
updateRequest: budgetRateBurnAlertUpdateRequest,
burnAlert: budgetRateBurnAlert,
},
}

t.Run("Delete", func(t *testing.T) {
err = c.BurnAlerts.Delete(ctx, dataset, burnAlert.ID)
for testName, testCase := range testCases {
var burnAlert *BurnAlert
var err error

t.Run(fmt.Sprintf("Create: %s", testName), func(t *testing.T) {
data := &testCase.createRequest
burnAlert, err = c.BurnAlerts.Create(ctx, dataset, data)

assert.NoError(t, err, "failed to create BurnAlert")
assert.NotNil(t, burnAlert.ID, "BurnAlert ID is empty")
assert.NotNil(t, burnAlert.CreatedAt, "created at is empty")
assert.NotNil(t, burnAlert.UpdatedAt, "updated at is empty")
assert.Equal(t, testCase.alertType, burnAlert.AlertType)

// copy dynamic fields before asserting equality
data.AlertType = burnAlert.AlertType
data.ID = burnAlert.ID
data.CreatedAt = burnAlert.CreatedAt
data.UpdatedAt = burnAlert.UpdatedAt
data.Recipients[0].ID = burnAlert.Recipients[0].ID
assert.Equal(t, data, burnAlert)
})

t.Run(fmt.Sprintf("Get: %s", testName), func(t *testing.T) {
result, err := c.BurnAlerts.Get(ctx, dataset, burnAlert.ID)
assert.NoError(t, err, "failed to get BurnAlert by ID")
assert.Equal(t, burnAlert, result)
})

t.Run(fmt.Sprintf("Update: %s", testName), func(t *testing.T) {
data := &testCase.updateRequest
data.ID = burnAlert.ID

burnAlert, err = c.BurnAlerts.Update(ctx, dataset, data)

assert.NoError(t, err, "failed to update BurnAlert")

// copy dynamic field before asserting equality
data.AlertType = burnAlert.AlertType
data.ID = burnAlert.ID
data.CreatedAt = burnAlert.CreatedAt
data.UpdatedAt = burnAlert.UpdatedAt
data.Recipients[0].ID = burnAlert.Recipients[0].ID
assert.Equal(t, burnAlert, data)
})

t.Run(fmt.Sprintf("ListForSLO: %s", testName), func(t *testing.T) {
results, err := c.BurnAlerts.ListForSLO(ctx, dataset, slo.ID)

assert.NoError(t, err, "failed to list burn alerts for SLO")
assert.NotZero(t, len(results))
assert.Equal(t, burnAlert.ID, results[0].ID, "newly created BurnAlert not in list of SLO's burn alerts")
})

t.Run(fmt.Sprintf("Delete - %s", testName), func(t *testing.T) {
err = c.BurnAlerts.Delete(ctx, dataset, burnAlert.ID)

assert.NoError(t, err, "failed to delete BurnAlert")
})

t.Run(fmt.Sprintf("Fail to GET a deleted burn alert: %s", testName), func(t *testing.T) {
_, err := c.BurnAlerts.Get(ctx, dataset, burnAlert.ID)

var de DetailedError
assert.Error(t, err)
assert.ErrorAs(t, err, &de)
assert.True(t, de.IsNotFound())
})
}
}

assert.NoError(t, err, "failed to delete BurnAlert")
})
func TestBurnAlerts_BurnAlertAlertTypes(t *testing.T) {
expectedAlertTypes := []BurnAlertAlertType{
BurnAlertAlertTypeExhaustionTime,
BurnAlertAlertTypeBudgetRate,
}

t.Run("Fail to Get deleted Burn Alert", func(t *testing.T) {
_, err := c.BurnAlerts.Get(ctx, dataset, burnAlert.ID)
t.Run("returns expected burn alert alert types", func(t *testing.T) {
actualAlertTypes := BurnAlertAlertTypes()

var de DetailedError
assert.Error(t, err)
assert.ErrorAs(t, err, &de)
assert.True(t, de.IsNotFound())
assert.NotEmpty(t, actualAlertTypes)
assert.Equal(t, len(expectedAlertTypes), len(actualAlertTypes))
assert.ElementsMatch(t, expectedAlertTypes, actualAlertTypes)
})
}
Loading