mirror of
https://github.com/prymitive/karma
synced 2026-08-23 11:56:20 +00:00
chore: move sorting to the backend
This commit is contained in:
@@ -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
|
||||
|
||||
//
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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" }
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user