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={