Skip to content

Commit b2837cb

Browse files
authored
Merge pull request #752 from slok/slok/ui
Allow filtering SLO lists on the UI domain layer
2 parents e8d8a9a + a316b87 commit b2837cb

File tree

3 files changed

+236
-16
lines changed

3 files changed

+236
-16
lines changed

internal/http/backend/app/slo.go

Lines changed: 46 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,13 @@ const (
2828
)
2929

3030
type ListSLOsRequest struct {
31-
FilterServiceID string // Used for filtering SLOs by service ID.
32-
FilterSearchInput string // Used for searching SLOs by name.
33-
SortMode SLOListSortMode
34-
Cursor string
31+
FilterServiceID string // Used for filtering SLOs by service ID.
32+
FilterSearchInput string // Used for searching SLOs by name.
33+
FilterAlertFiring bool // Used for filtering SLOs that have firing alerts.
34+
FilterPeriodBudgetConsumed bool // Used for filtering SLOs that have burned budget above threshold.
35+
FilterCurrentBurningBudgetOver100 bool // Used for filtering SLOs that are currently burning budget over 100%.
36+
SortMode SLOListSortMode
37+
Cursor string
3538
}
3639

3740
func (r *ListSLOsRequest) defaults() error {
@@ -80,72 +83,99 @@ func (a *App) ListSLOs(ctx context.Context, req ListSLOsRequest) (*ListSLOsRespo
8083
}
8184
}
8285

86+
// Filter SLOs if required.
87+
filters := []sloFilter{}
88+
if req.FilterAlertFiring {
89+
filters = append(filters, filterIncludeSLOWithAlerts(true, true))
90+
}
91+
if req.FilterPeriodBudgetConsumed {
92+
filters = append(filters, filterIncludeSLOsWithWindowBudgetConsumed(100))
93+
}
94+
if req.FilterCurrentBurningBudgetOver100 {
95+
filters = append(filters, filterIncludeSLOsCurrentBurningBudgetOverThreshold(100))
96+
}
97+
98+
filteredSLOs := slos
99+
if len(filters) > 0 {
100+
filterChain := newSLOFilterChain(filters...)
101+
filteredSLOs = []storage.SLOInstantDetails{}
102+
for _, slo := range slos {
103+
include, err := filterChain.IncludeSLO(ctx, &slo)
104+
if err != nil {
105+
return nil, fmt.Errorf("could not filter SLOs: %w", err)
106+
}
107+
if include {
108+
filteredSLOs = append(filteredSLOs, slo)
109+
}
110+
}
111+
}
112+
83113
// Sort results based on request.
84114

85115
// Always sort by SLO ID first to have a stable sort.
86-
slices.SortStableFunc(slos, func(x, y storage.SLOInstantDetails) int {
116+
slices.SortStableFunc(filteredSLOs, func(x, y storage.SLOInstantDetails) int {
87117
return strings.Compare(x.SLO.ID, y.SLO.ID)
88118
})
89119

90120
switch req.SortMode {
91121
case SLOListSortModeSLOIDDesc:
92-
slices.SortStableFunc(slos, func(x, y storage.SLOInstantDetails) int {
122+
slices.SortStableFunc(filteredSLOs, func(x, y storage.SLOInstantDetails) int {
93123
return strings.Compare(y.SLO.ID, x.SLO.ID)
94124
})
95125
case SLOListSortModeServiceNameAsc:
96-
slices.SortStableFunc(slos, func(x, y storage.SLOInstantDetails) int {
126+
slices.SortStableFunc(filteredSLOs, func(x, y storage.SLOInstantDetails) int {
97127
return strings.Compare(x.SLO.ServiceID, y.SLO.ServiceID)
98128
})
99129
case SLOListSortModeServiceNameDesc:
100-
slices.SortStableFunc(slos, func(x, y storage.SLOInstantDetails) int {
130+
slices.SortStableFunc(filteredSLOs, func(x, y storage.SLOInstantDetails) int {
101131
return strings.Compare(y.SLO.ServiceID, x.SLO.ServiceID)
102132
})
103133
case SLOListSortModeCurrentBurningBudgetAsc:
104-
slices.SortStableFunc(slos, func(x, y storage.SLOInstantDetails) int {
134+
slices.SortStableFunc(filteredSLOs, func(x, y storage.SLOInstantDetails) int {
105135
return cmp.Compare(
106136
x.BudgetDetails.BurningBudgetPercent,
107137
y.BudgetDetails.BurningBudgetPercent,
108138
)
109139
})
110140
case SLOListSortModeCurrentBurningBudgetDesc:
111-
slices.SortStableFunc(slos, func(x, y storage.SLOInstantDetails) int {
141+
slices.SortStableFunc(filteredSLOs, func(x, y storage.SLOInstantDetails) int {
112142
return cmp.Compare(
113143
y.BudgetDetails.BurningBudgetPercent,
114144
x.BudgetDetails.BurningBudgetPercent,
115145
)
116146
})
117147
case SLOListSortModeBudgetBurnedWindowPeriodAsc:
118-
slices.SortStableFunc(slos, func(x, y storage.SLOInstantDetails) int {
148+
slices.SortStableFunc(filteredSLOs, func(x, y storage.SLOInstantDetails) int {
119149
return cmp.Compare(
120150
x.BudgetDetails.BurnedBudgetWindowPercent,
121151
y.BudgetDetails.BurnedBudgetWindowPercent,
122152
)
123153
})
124154
case SLOListSortModeBudgetBurnedWindowPeriodDesc:
125-
slices.SortStableFunc(slos, func(x, y storage.SLOInstantDetails) int {
155+
slices.SortStableFunc(filteredSLOs, func(x, y storage.SLOInstantDetails) int {
126156
return cmp.Compare(
127157
y.BudgetDetails.BurnedBudgetWindowPercent,
128158
x.BudgetDetails.BurnedBudgetWindowPercent,
129159
)
130160
})
131161
case SLOListSortModeAlertSeverityAsc:
132-
slices.SortStableFunc(slos, func(x, y storage.SLOInstantDetails) int {
162+
slices.SortStableFunc(filteredSLOs, func(x, y storage.SLOInstantDetails) int {
133163
return cmp.Compare(
134164
getAlertSeverityScore([]model.SLOAlerts{x.Alerts}),
135165
getAlertSeverityScore([]model.SLOAlerts{y.Alerts}),
136166
)
137167
})
138168
case SLOListSortModeAlertSeverityDesc:
139-
slices.SortStableFunc(slos, func(x, y storage.SLOInstantDetails) int {
169+
slices.SortStableFunc(filteredSLOs, func(x, y storage.SLOInstantDetails) int {
140170
return cmp.Compare(
141171
getAlertSeverityScore([]model.SLOAlerts{y.Alerts}),
142172
getAlertSeverityScore([]model.SLOAlerts{x.Alerts}),
143173
)
144174
})
145175
}
146176

147-
rtSLOs := make([]RealTimeSLODetails, 0, len(slos))
148-
for _, s := range slos {
177+
rtSLOs := make([]RealTimeSLODetails, 0, len(filteredSLOs))
178+
for _, s := range filteredSLOs {
149179
rtSLOs = append(rtSLOs, RealTimeSLODetails{
150180
SLO: s.SLO,
151181
Alerts: s.Alerts,
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package app
2+
3+
import (
4+
"context"
5+
6+
"github.com/slok/sloth/internal/http/backend/storage"
7+
)
8+
9+
type sloFilter interface {
10+
IncludeSLO(ctx context.Context, slo *storage.SLOInstantDetails) (bool, error)
11+
}
12+
13+
type sloFilterFunc func(ctx context.Context, slo *storage.SLOInstantDetails) (bool, error)
14+
15+
func (f sloFilterFunc) IncludeSLO(ctx context.Context, slo *storage.SLOInstantDetails) (bool, error) {
16+
return f(ctx, slo)
17+
}
18+
19+
func newSLOFilterChain(filters ...sloFilter) sloFilter {
20+
return sloFilterFunc(func(ctx context.Context, slo *storage.SLOInstantDetails) (bool, error) {
21+
for _, filter := range filters {
22+
include, err := filter.IncludeSLO(ctx, slo)
23+
if err != nil {
24+
return false, err
25+
}
26+
if !include {
27+
return false, nil
28+
}
29+
}
30+
return true, nil
31+
})
32+
}
33+
34+
// filterIncludeSLOWithAlerts filters in SLOs that have firing alerts.
35+
func filterIncludeSLOWithAlerts(page, warning bool) sloFilter {
36+
return sloFilterFunc(func(ctx context.Context, slo *storage.SLOInstantDetails) (bool, error) {
37+
if slo.Alerts.FiringPage != nil && page {
38+
return true, nil
39+
}
40+
41+
if slo.Alerts.FiringWarning != nil && warning {
42+
return true, nil
43+
}
44+
45+
return false, nil
46+
})
47+
}
48+
49+
// filterIncludeSLOsWithWindowBudgetConsumed filters in SLOs that have window budget consumed above threshold.
50+
func filterIncludeSLOsWithWindowBudgetConsumed(threshold float64) sloFilter {
51+
return sloFilterFunc(func(ctx context.Context, slo *storage.SLOInstantDetails) (bool, error) {
52+
if slo.BudgetDetails.BurnedBudgetWindowPercent > threshold {
53+
return true, nil
54+
}
55+
56+
return false, nil
57+
})
58+
}
59+
60+
// filterIncludeSLOsCurrentBurningBudgetOverThreshold filters in SLOs that are currently burning budget over the given threshold.
61+
func filterIncludeSLOsCurrentBurningBudgetOverThreshold(threshold float64) sloFilter {
62+
return sloFilterFunc(func(ctx context.Context, slo *storage.SLOInstantDetails) (bool, error) {
63+
if slo.BudgetDetails.BurningBudgetPercent > threshold {
64+
return true, nil
65+
}
66+
67+
return false, nil
68+
})
69+
}

internal/http/backend/app/slo_test.go

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -677,6 +677,127 @@ func TestListSLOs(t *testing.T) {
677677
}
678678
},
679679
},
680+
681+
"Filtering SLOS with firing alerts should return only those with firing alerts.": {
682+
req: app.ListSLOsRequest{
683+
FilterAlertFiring: true,
684+
},
685+
mock: func(m *storagemock.SLOGetter) {
686+
m.On("ListSLOInstantDetails", mock.Anything).Return([]storage.SLOInstantDetails{
687+
{
688+
SLO: model.SLO{ID: "slo-1"},
689+
BudgetDetails: model.SLOBudgetDetails{},
690+
Alerts: model.SLOAlerts{FiringWarning: &model.Alert{Name: "slo-1-warning"}},
691+
},
692+
{
693+
SLO: model.SLO{ID: "slo-2"},
694+
BudgetDetails: model.SLOBudgetDetails{},
695+
Alerts: model.SLOAlerts{},
696+
},
697+
{
698+
SLO: model.SLO{ID: "slo-3"},
699+
BudgetDetails: model.SLOBudgetDetails{},
700+
Alerts: model.SLOAlerts{FiringPage: &model.Alert{Name: "slo-3-critical"}},
701+
},
702+
}, nil)
703+
},
704+
expResp: func() *app.ListSLOsResponse {
705+
return &app.ListSLOsResponse{
706+
SLOs: []app.RealTimeSLODetails{
707+
{
708+
SLO: model.SLO{ID: "slo-1"},
709+
Budget: model.SLOBudgetDetails{},
710+
Alerts: model.SLOAlerts{FiringWarning: &model.Alert{Name: "slo-1-warning"}},
711+
},
712+
{
713+
SLO: model.SLO{ID: "slo-3"},
714+
Budget: model.SLOBudgetDetails{},
715+
Alerts: model.SLOAlerts{FiringPage: &model.Alert{Name: "slo-3-critical"}},
716+
},
717+
},
718+
}
719+
},
720+
},
721+
722+
"Filtering SLOS with window budget consumed should return only those with window budget consumed above threshold.": {
723+
req: app.ListSLOsRequest{
724+
FilterPeriodBudgetConsumed: true,
725+
},
726+
mock: func(m *storagemock.SLOGetter) {
727+
m.On("ListSLOInstantDetails", mock.Anything).Return([]storage.SLOInstantDetails{
728+
{
729+
SLO: model.SLO{ID: "slo-1"},
730+
BudgetDetails: model.SLOBudgetDetails{
731+
BurnedBudgetWindowPercent: 75.0,
732+
},
733+
},
734+
{
735+
SLO: model.SLO{ID: "slo-2"},
736+
BudgetDetails: model.SLOBudgetDetails{
737+
BurnedBudgetWindowPercent: 101.0,
738+
},
739+
},
740+
{
741+
SLO: model.SLO{ID: "slo-3"},
742+
BudgetDetails: model.SLOBudgetDetails{
743+
BurnedBudgetWindowPercent: 90.0,
744+
},
745+
},
746+
}, nil)
747+
},
748+
expResp: func() *app.ListSLOsResponse {
749+
return &app.ListSLOsResponse{
750+
SLOs: []app.RealTimeSLODetails{
751+
{
752+
SLO: model.SLO{ID: "slo-2"},
753+
Budget: model.SLOBudgetDetails{BurnedBudgetWindowPercent: 101},
754+
},
755+
},
756+
}
757+
},
758+
},
759+
760+
"Filtering SLOS with current burning budget over 100% should return only those with current burning budget over threshold.": {
761+
req: app.ListSLOsRequest{
762+
FilterCurrentBurningBudgetOver100: true,
763+
},
764+
mock: func(m *storagemock.SLOGetter) {
765+
m.On("ListSLOInstantDetails", mock.Anything).Return([]storage.SLOInstantDetails{
766+
{
767+
SLO: model.SLO{ID: "slo-1"},
768+
BudgetDetails: model.SLOBudgetDetails{
769+
BurningBudgetPercent: 75.0,
770+
},
771+
},
772+
{
773+
SLO: model.SLO{ID: "slo-2"},
774+
BudgetDetails: model.SLOBudgetDetails{
775+
BurningBudgetPercent: 121.0,
776+
},
777+
},
778+
{
779+
SLO: model.SLO{ID: "slo-3"},
780+
BudgetDetails: model.SLOBudgetDetails{
781+
BurningBudgetPercent: 100.1,
782+
},
783+
},
784+
}, nil)
785+
},
786+
expResp: func() *app.ListSLOsResponse {
787+
return &app.ListSLOsResponse{
788+
SLOs: []app.RealTimeSLODetails{
789+
{
790+
SLO: model.SLO{ID: "slo-2"},
791+
Budget: model.SLOBudgetDetails{BurningBudgetPercent: 121},
792+
},
793+
{
794+
SLO: model.SLO{ID: "slo-3"},
795+
Budget: model.SLOBudgetDetails{BurningBudgetPercent: 100.1},
796+
},
797+
},
798+
}
799+
},
800+
},
680801
}
681802
for name, test := range tests {
682803
t.Run(name, func(t *testing.T) {

0 commit comments

Comments
 (0)