chore: move sorting to the backend

This commit is contained in:
Łukasz Mierzwa
2019-08-08 15:45:00 +01:00
parent 63fdca097d
commit 9b6273595d
9 changed files with 146 additions and 41 deletions
+77
View File
@@ -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
//
}
-7
View File
@@ -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
}
+12
View File
@@ -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
}
+1 -1
View File
@@ -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"`
+18 -3
View File
@@ -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() {
+3 -7
View File
@@ -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={
<div key="loader" className="text-center text-muted py-3">
@@ -257,8 +254,7 @@ const AlertGrid = observer(
</div>
}
>
{Object.values(alertStore.data.groups)
.sort(this.compare)
{alertStore.data.groups
.slice(0, this.groupsToRender.value)
.map(group => (
<AlertGroup
+19 -15
View File
@@ -138,7 +138,7 @@ class AlertStore {
{
colors: {},
counters: [],
groups: {},
groups: [],
silences: {},
upstreams: { instances: [], clusters: {} },
getAlertmanagerByName(name) {
@@ -316,22 +316,26 @@ class AlertStore {
// update groups, it can be huge so we have custom logic with cheaper
// comparision logic running per group using content hashes from the API
// response
for (const key of Object.keys(result.groups)) {
// set/update each group if:
// * it's not yet stored in AlertStore
// * it's stored but hash is different than in the API response
if (
!(key in this.data.groups) ||
(key in this.data.groups &&
result.groups[key].hash !== this.data.groups[key].hash)
) {
this.data.groups[key] = result.groups[key];
const storedGroups = {};
for (const [index, group] of Object.entries(this.data.groups)) {
storedGroups[group.id] = index;
}
for (const group of result.groups) {
const index = storedGroups[group.id];
if (index !== undefined) {
const storedGroup = this.data.groups[index];
if (storedGroup && storedGroup.hash !== group.hash) {
this.data.groups[index] = group;
}
} else {
this.data.groups.push(group);
}
}
for (const key of Object.keys(this.data.groups).filter(
k => !(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
+13 -7
View File
@@ -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" }
]);
});
});
+3 -1
View File
@@ -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)