From 9b6273595dc6b1416e1b12f0ea90274ae03a54fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Fri, 26 Jul 2019 21:40:26 +0100 Subject: [PATCH] chore: move sorting to the backend --- alerts.go | 77 +++++++++++++++++++++++ internal/alertmanager/dedup.go | 7 --- internal/models/alertgroup.go | 12 ++++ internal/models/api.go | 2 +- ui/src/Components/Fetcher/index.js | 21 ++++++- ui/src/Components/Grid/AlertGrid/index.js | 10 +-- ui/src/Stores/AlertStore.js | 34 +++++----- ui/src/Stores/AlertStore.test.js | 20 +++--- views.go | 4 +- 9 files changed, 146 insertions(+), 41 deletions(-) diff --git a/alerts.go b/alerts.go index c69550aba..22d457fd8 100644 --- a/alerts.go +++ b/alerts.go @@ -5,7 +5,9 @@ import ( "math" "sort" + "github.com/gin-gonic/gin" "github.com/prymitive/karma/internal/alertmanager" + "github.com/prymitive/karma/internal/config" "github.com/prymitive/karma/internal/filters" "github.com/prymitive/karma/internal/models" "github.com/prymitive/karma/internal/slices" @@ -125,3 +127,78 @@ func getUpstreams() models.AlertmanagerAPISummary { return summary } + +func resolveLabelValue(name, value string) (int, bool) { + valueReplacements, found := config.Config.Grid.Sorting.CustomValues.Labels[name] + if found { + if replacement, ok := valueReplacements[value]; ok { + return replacement, true + } + } + return value, false +} + +func getGroupLabel(group *models.APIAlertGroup, label string) int { + if v, found := group.Labels[label]; found { + return resolveLabelValue(label, v) + } + if v, found := group.Shared.Labels[label]; found { + return resolveLabelValue(label, v) + } + if v, found := group.Alerts[0].Labels[label]; found { + return resolveLabelValue(label, v) + } + return 0 +} + +func sortAlertGroups(c *gin.Context, groupsMap map[string]models.APIAlertGroup) []models.APIAlertGroup { + groups := make([]models.APIAlertGroup, 0, len(groupsMap)) + + sortOrder, found := c.GetQuery("sortOrder") + if !found { + sortOrder = config.Config.Grid.Sorting.Order + } + + sortReverse, found := c.GetQuery("sortReverse") + if !found { + if config.Config.Grid.Sorting.Reverse { + sortReverse = "1" + } else { + sortReverse = "0" + } + } + + sortLabel, found := c.GetQuery("sortLabel") + if !found { + sortLabel = config.Config.Grid.Sorting.Label + } + + for _, g := range groupsMap { + groups = append(groups, g) + } + + switch sortOrder { + case "startsAt": + sort.SliceStable(groups, func(i, j int) bool { + return groups[i].LatestStartsAt.After(groups[j].LatestStartsAt) + }) + case "label": + sort.SliceStable(groups, func(i, j int) bool { + return getGroupLabel(&groups[i], sortLabel) < getGroupLabel(&groups[j], sortLabel) + }) + default: + // sort alert groups so they are always returned in the same order + // use group ID which is unique and immutable + sort.SliceStable(groups, func(i, j int) bool { + return groups[i].ID < groups[j].ID + }) + } + + if sortReverse == "1" { + sort.Reverse(groups) + } + + return groups + + // +} diff --git a/internal/alertmanager/dedup.go b/internal/alertmanager/dedup.go index 5355671a9..77a85c480 100644 --- a/internal/alertmanager/dedup.go +++ b/internal/alertmanager/dedup.go @@ -89,17 +89,10 @@ func DedupAlerts() []models.AlertGroup { }) ag.Alerts = append(ag.Alerts, alert) } - sort.Sort(ag.Alerts) ag.Hash = ag.ContentFingerprint() dedupedGroups = append(dedupedGroups, ag) } - // sort alert groups so they are always returned in the same order - // use group ID which is unique and immutable - sort.Slice(dedupedGroups, func(i, j int) bool { - return dedupedGroups[i].ID < dedupedGroups[j].ID - }) - return dedupedGroups } diff --git a/internal/models/alertgroup.go b/internal/models/alertgroup.go index d79a656f1..8d07ba1b7 100644 --- a/internal/models/alertgroup.go +++ b/internal/models/alertgroup.go @@ -4,6 +4,7 @@ import ( "crypto/sha1" "fmt" "io" + "time" "github.com/cnf/structhash" @@ -42,6 +43,7 @@ type AlertGroup struct { Hash string `json:"hash"` AlertmanagerCount map[string]int `json:"alertmanagerCount"` StateCount map[string]int `json:"stateCount"` + LatestStartsAt time.Time `json:"-"` } // LabelsFingerprint is a checksum of this AlertGroup labels and the receiver @@ -73,3 +75,13 @@ func (ag AlertGroup) ContentFingerprint() string { } return fmt.Sprintf("%x", h.Sum(nil)) } + +func (ag AlertGroup) FindLatestStartsAt() time.Time { + var ts time.Time + for i, alert := range ag.Alerts { + if i == 0 || alert.StartsAt.After(ts) { + ts = alert.StartsAt + } + } + return ts +} diff --git a/internal/models/api.go b/internal/models/api.go index 60b9f266e..88f227208 100644 --- a/internal/models/api.go +++ b/internal/models/api.go @@ -293,7 +293,7 @@ type AlertsResponse struct { Version string `json:"version"` Upstreams AlertmanagerAPISummary `json:"upstreams"` Silences map[string]map[string]Silence `json:"silences"` - AlertGroups map[string]APIAlertGroup `json:"groups"` + AlertGroups []APIAlertGroup `json:"groups"` TotalAlerts int `json:"totalAlerts"` Colors LabelsColorMap `json:"colors"` Filters []Filter `json:"filters"` diff --git a/ui/src/Components/Fetcher/index.js b/ui/src/Components/Fetcher/index.js index acaae31bd..ce0d87c5c 100644 --- a/ui/src/Components/Fetcher/index.js +++ b/ui/src/Components/Fetcher/index.js @@ -19,18 +19,27 @@ const Fetcher = observer( lastTick = observable( { time: moment(0), + completedAt: moment(0), update() { this.time = moment(); + }, + markCompleted() { + this.completedAt = moment(); } }, { - update: action + update: action, + markCompleted: action } ); fetchIfIdle = () => { const { alertStore, settingsStore } = this.props; + // add 5s minimum interval between fetches + const idleAt = moment(this.lastTick.completedAt).add(5, "seconds"); + const isIdle = moment().isSameOrAfter(idleAt); + const nextTick = moment(this.lastTick.time).add( settingsStore.fetchConfig.config.interval, "seconds" @@ -43,14 +52,20 @@ const Fetcher = observer( status === AlertStoreStatuses.Fetching.toString() || status === AlertStoreStatuses.Processing.toString(); - if (pastDeadline && !updateInProgress && !alertStore.status.paused) { + if ( + isIdle && + pastDeadline && + !updateInProgress && + !alertStore.status.paused + ) { this.lastTick.update(); alertStore.fetchWithThrottle(); + this.lastTick.markCompleted(); } }; timerTick = () => { - this.fetchIfIdle(); + window.requestAnimationFrame(this.fetchIfIdle); }; componentDidMount() { diff --git a/ui/src/Components/Grid/AlertGrid/index.js b/ui/src/Components/Grid/AlertGrid/index.js index 54f6cafe5..a4143718a 100644 --- a/ui/src/Components/Grid/AlertGrid/index.js +++ b/ui/src/Components/Grid/AlertGrid/index.js @@ -147,7 +147,7 @@ const AlertGrid = observer( this.groupsToRender.value = Math.min( this.groupsToRender.value + this.loadMoreStep, - Object.keys(alertStore.data.groups).length + alertStore.data.groups.length ); }); @@ -246,10 +246,7 @@ const AlertGrid = observer( pack={true} sizes={this.viewport.gridSizesConfig} loadMore={this.loadMore} - hasMore={ - this.groupsToRender.value < - Object.keys(alertStore.data.groups).length - } + hasMore={this.groupsToRender.value < alertStore.data.groups.length} threshold={50} loader={
@@ -257,8 +254,7 @@ const AlertGrid = observer(
} > - {Object.values(alertStore.data.groups) - .sort(this.compare) + {alertStore.data.groups .slice(0, this.groupsToRender.value) .map(group => ( !(k in result.groups) - )) { - delete this.data.groups[key]; + const knownGroups = result.groups.map(g => g.id); + for (const [index, group] of Object.entries(this.data.groups)) { + if (!knownGroups.includes(group.id)) { + delete this.data.groups[index]; + } } // before storing new version check if we need to reload diff --git a/ui/src/Stores/AlertStore.test.js b/ui/src/Stores/AlertStore.test.js index 2d692c927..3f82a11a1 100644 --- a/ui/src/Stores/AlertStore.test.js +++ b/ui/src/Stores/AlertStore.test.js @@ -373,16 +373,22 @@ describe("AlertStore.fetch", () => { it("updates groups with new hash after fetch", () => { const store = new AlertStore(["label=value"]); - store.data.groups = { foo: { hash: "foo" }, bar: { hash: "bar" } }; + store.data.groups = [ + { id: "foo", hash: "foo" }, + { id: "bar", hash: "bar" } + ]; const response = EmptyAPIResponse(); - response.groups = { foo: { hash: "newFoo" }, bar: { hash: "newBar" } }; + response.groups = [ + { id: "foo", hash: "newFoo" }, + { id: "bar", hash: "newBar" } + ]; store.parseAPIResponse(response); - expect(Object.keys(store.data.groups)).toHaveLength(2); - expect(store.data.groups).toMatchObject({ - foo: { hash: "newFoo" }, - bar: { hash: "newBar" } - }); + expect(store.data.groups).toHaveLength(2); + expect(store.data.groups).toMatchObject([ + { id: "foo", hash: "newFoo" }, + { id: "bar", hash: "newBar" } + ]); }); }); diff --git a/views.go b/views.go index 5d4b94785..5ac5de515 100644 --- a/views.go +++ b/views.go @@ -220,6 +220,7 @@ func alerts(c *gin.Context) { ID: ag.ID, Receiver: ag.Receiver, Labels: ag.Labels, + LatestStartsAt: ag.LatestStartsAt, Alerts: []models.Alert{}, AlertmanagerCount: map[string]int{}, StateCount: map[string]int{}, @@ -311,6 +312,7 @@ func alerts(c *gin.Context) { } } } + agCopy.LatestStartsAt = agCopy.FindLatestStartsAt() agCopy.Hash = agCopy.ContentFingerprint() apiAG := models.APIAlertGroup{AlertGroup: agCopy} apiAG.DedupSharedMaps() @@ -326,7 +328,7 @@ func alerts(c *gin.Context) { } } - resp.AlertGroups = alerts + resp.AlertGroups = sortAlertGroups(c, alerts) resp.Silences = silences resp.Colors = colors resp.Counters = countersToLabelStats(counters)