Skip to content
40 changes: 40 additions & 0 deletions api/v2/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ package v2

import (
"bytes"
"context"
"encoding/json"
"io"
"net/http"
Expand Down Expand Up @@ -604,6 +605,45 @@ receivers:
}
}

func BenchmarkOpenAPIAlertsToAlerts(b *testing.B) {
now := strfmt.DateTime(time.Now())
apiAlerts := make(open_api_models.PostableAlerts, 100)
for i := range apiAlerts {
apiAlerts[i] = &open_api_models.PostableAlert{
Alert: open_api_models.Alert{
Labels: open_api_models.LabelSet{"alertname": "test", "i": strconv.Itoa(i)},
},
StartsAt: now,
EndsAt: now,
}
}

b.Run("PreAllocated", func(b *testing.B) {
ctx := context.Background()
for i := 0; i < b.N; i++ {
OpenAPIAlertsToAlerts(ctx, apiAlerts)
}
})

b.Run("AppendGrowth", func(b *testing.B) {
for i := 0; i < b.N; i++ {
alerts := []*types.Alert{}
for _, apiAlert := range apiAlerts {
alerts = append(alerts, &types.Alert{
Alert: model.Alert{
Labels: APILabelSetToModelLabelSet(apiAlert.Labels),
Annotations: APILabelSetToModelLabelSet(apiAlert.Annotations),
StartsAt: time.Time(apiAlert.StartsAt),
EndsAt: time.Time(apiAlert.EndsAt),
GeneratorURL: string(apiAlert.GeneratorURL),
},
})
}
_ = alerts
}
})
}

func TestPostSilences_QuotedMatchers(t *testing.T) {
// This test ensures that quoted values in matchers are preserved during JSON unmarshalling
jsonBlob := `{"comment":"foo", "createdBy": "author", "startsAt":"2023-03-06T00:22:15Z", "endsAt":"2024-03-06T00:22:15Z", "matchers":[{"isRegex":true, "name":"instance", "value":"\"bar\""}]}`
Expand Down
7 changes: 3 additions & 4 deletions api/v2/compat.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,18 +196,17 @@ func OpenAPIAlertsToAlerts(ctx context.Context, apiAlerts open_api_models.Postab
_, span := tracer.Start(ctx, "OpenAPIAlertsToAlerts")
defer span.End()

alerts := []*types.Alert{}
alerts := make([]*types.Alert, 0, len(apiAlerts))
for _, apiAlert := range apiAlerts {
alert := types.Alert{
alerts = append(alerts, &types.Alert{
Alert: prometheus_model.Alert{
Labels: APILabelSetToModelLabelSet(apiAlert.Labels),
Annotations: APILabelSetToModelLabelSet(apiAlert.Annotations),
StartsAt: time.Time(apiAlert.StartsAt),
EndsAt: time.Time(apiAlert.EndsAt),
GeneratorURL: string(apiAlert.GeneratorURL),
},
}
alerts = append(alerts, &alert)
})
}

return alerts
Expand Down
6 changes: 5 additions & 1 deletion dispatch/dispatch.go
Original file line number Diff line number Diff line change
Expand Up @@ -589,7 +589,11 @@ func (d *Dispatcher) runAG(ag *aggrGroup) {
}

func getGroupLabels(alert *types.Alert, route *Route) model.LabelSet {
groupLabels := model.LabelSet{}
capacity := len(route.RouteOpts.GroupBy)
if route.RouteOpts.GroupByAll {
capacity = len(alert.Labels)
}
groupLabels := make(model.LabelSet, capacity)
for ln, lv := range alert.Labels {
if _, ok := route.RouteOpts.GroupBy[ln]; ok || route.RouteOpts.GroupByAll {
groupLabels[ln] = lv
Expand Down
97 changes: 97 additions & 0 deletions dispatch/dispatch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1086,3 +1086,100 @@ func TestDispatchOnStartup(t *testing.T) {
require.True(t, fingerprints[alert1.Fingerprint()], "expected alert1 to be present")
require.True(t, fingerprints[alert2.Fingerprint()], "expected alert2 to be present")
}

func TestGetGroupLabels(t *testing.T) {
alert := &types.Alert{
Alert: model.Alert{
Labels: model.LabelSet{
"alertname": "TestAlert",
"job": "prometheus",
"instance": "localhost:9090",
"severity": "critical",
},
},
}

t.Run("specific labels", func(t *testing.T) {
route := &Route{
RouteOpts: RouteOpts{
GroupBy: map[model.LabelName]struct{}{
"alertname": {},
"job": {},
},
},
}
labels := getGroupLabels(alert, route)
require.Len(t, labels, 2)
require.Equal(t, model.LabelValue("TestAlert"), labels["alertname"])
require.Equal(t, model.LabelValue("prometheus"), labels["job"])
})

t.Run("group by all", func(t *testing.T) {
route := &Route{
RouteOpts: RouteOpts{
GroupByAll: true,
},
}
labels := getGroupLabels(alert, route)
require.Len(t, labels, 4)
require.Equal(t, alert.Labels, labels)
})
}

func BenchmarkGetGroupLabels(b *testing.B) {
now := time.Now()

// Alert with many labels (typical production alert)
alert := &types.Alert{
Alert: model.Alert{
Labels: model.LabelSet{
"alertname": "TestAlert",
"severity": "critical",
"job": "prometheus",
"instance": "localhost:9090",
"namespace": "monitoring",
"cluster": "prod-us-east-1",
"datacenter": "dc1",
"env": "production",
"team": "platform",
"service": "alertmanager",
},
StartsAt: now.Add(-time.Hour),
EndsAt: now.Add(time.Hour),
},
}

b.Run("specific_labels", func(b *testing.B) {
route := &Route{
RouteOpts: RouteOpts{
GroupBy: map[model.LabelName]struct{}{
"alertname": {},
"job": {},
"severity": {},
},
},
}

b.ResetTimer()
b.ReportAllocs()

for i := 0; i < b.N; i++ {
_ = getGroupLabels(alert, route)
}
})

b.Run("group_by_all", func(b *testing.B) {
route := &Route{
RouteOpts: RouteOpts{
GroupByAll: true,
},
}

b.ResetTimer()
b.ReportAllocs()

for i := 0; i < b.N; i++ {
_ = getGroupLabels(alert, route)
}
})
}
2 changes: 1 addition & 1 deletion inhibit/inhibit.go
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ func NewInhibitRule(cr config.InhibitRule) *InhibitRule {

// fingerprintEquals returns the fingerprint of the equal labels of the given label set.
func (r *InhibitRule) fingerprintEquals(lset model.LabelSet) model.Fingerprint {
equalSet := model.LabelSet{}
equalSet := make(model.LabelSet, len(r.Equal))
for n := range r.Equal {
equalSet[n] = lset[n]
}
Expand Down
62 changes: 62 additions & 0 deletions inhibit/inhibit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ package inhibit

import (
"context"
"fmt"
"testing"
"time"

Expand Down Expand Up @@ -557,3 +558,64 @@ func TestInhibit(t *testing.T) {
}
}
}

func TestInhibitRule_fingerprintEquals(t *testing.T) {
rule := &InhibitRule{
Equal: map[model.LabelName]struct{}{
"cluster": {},
"service": {},
},
}

lset := model.LabelSet{
"cluster": "prod",
"service": "api",
"instance": "host1",
}

fp := rule.fingerprintEquals(lset)

// Same equal labels should produce same fingerprint
lset2 := model.LabelSet{
"cluster": "prod",
"service": "api",
"instance": "host2", // different non-equal label
}
require.Equal(t, fp, rule.fingerprintEquals(lset2))

// Different equal label value should produce different fingerprint
lset3 := model.LabelSet{
"cluster": "staging",
"service": "api",
}
require.NotEqual(t, fp, rule.fingerprintEquals(lset3))
}

func BenchmarkFingerprintEquals(b *testing.B) {
// Test fingerprintEquals with varying number of equal labels
for _, numLabels := range []int{1, 3, 5, 10} {
b.Run(fmt.Sprintf("%d_equal_labels", numLabels), func(b *testing.B) {
equalLabels := make(map[model.LabelName]struct{}, numLabels)
for i := range numLabels {
equalLabels[model.LabelName(fmt.Sprintf("label_%d", i))] = struct{}{}
}

rule := &InhibitRule{Equal: equalLabels}

// Create a label set with matching values
lset := make(model.LabelSet, numLabels+2)
lset["source"] = "true"
lset["target"] = "true"
for i := range numLabels {
lset[model.LabelName(fmt.Sprintf("label_%d", i))] = model.LabelValue(fmt.Sprintf("value_%d", i))
}

b.ResetTimer()
b.ReportAllocs()

for b.Loop() {
_ = rule.fingerprintEquals(lset)
}
})
}
}
37 changes: 21 additions & 16 deletions notify/notify.go
Original file line number Diff line number Diff line change
Expand Up @@ -751,6 +751,26 @@ func (n *DedupStage) needsUpdate(entry *nflogpb.Entry, firing, resolved map[uint
return ReasonDoNotNotify
}

// partitionAlertsByState separates alerts into firing and resolved, returning both slices and sets.
func partitionAlertsByState(alerts []*types.Alert, hashFn func(*types.Alert) uint64) (firing, resolved []uint64, firingSet, resolvedSet map[uint64]struct{}) {
firingSet = make(map[uint64]struct{}, len(alerts))
resolvedSet = make(map[uint64]struct{}, len(alerts))
firing = make([]uint64, 0, len(alerts))
resolved = make([]uint64, 0, len(alerts))

for _, a := range alerts {
hash := hashFn(a)
if a.Resolved() {
resolved = append(resolved, hash)
resolvedSet[hash] = struct{}{}
} else {
firing = append(firing, hash)
firingSet[hash] = struct{}{}
}
}
return firing, resolved, firingSet, resolvedSet
}

// Exec implements the Stage interface.
func (n *DedupStage) Exec(ctx context.Context, _ *slog.Logger, alerts ...*types.Alert) (context.Context, []*types.Alert, error) {
gkey, ok := GroupKey(ctx)
Expand All @@ -770,22 +790,7 @@ func (n *DedupStage) Exec(ctx context.Context, _ *slog.Logger, alerts ...*types.
return ctx, nil, errors.New("repeat interval missing")
}

firingSet := map[uint64]struct{}{}
resolvedSet := map[uint64]struct{}{}
firing := []uint64{}
resolved := []uint64{}

var hash uint64
for _, a := range alerts {
hash = n.hash(a)
if a.Resolved() {
resolved = append(resolved, hash)
resolvedSet[hash] = struct{}{}
} else {
firing = append(firing, hash)
firingSet[hash] = struct{}{}
}
}
firing, resolved, firingSet, resolvedSet := partitionAlertsByState(alerts, n.hash)

ctx = WithFiringAlerts(ctx, firing)
ctx = WithResolvedAlerts(ctx, resolved)
Expand Down
6 changes: 4 additions & 2 deletions template/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -376,9 +376,11 @@ func (as Alerts) Resolved() []Alert {

// Data assembles data for template expansion.
func (t *Template) Data(recv string, groupLabels model.LabelSet, notificationReason string, alerts ...*types.Alert) *Data {
typedAlerts := types.Alerts(alerts...)

data := &Data{
Receiver: regexp.QuoteMeta(recv),
Status: string(types.Alerts(alerts...).Status()),
Status: string(typedAlerts.Status()),
Alerts: make(Alerts, 0, len(alerts)),
NotificationReason: notificationReason,
GroupLabels: KV{},
Expand All @@ -389,7 +391,7 @@ func (t *Template) Data(recv string, groupLabels model.LabelSet, notificationRea

// The call to types.Alert is necessary to correctly resolve the internal
// representation to the user representation.
for _, a := range types.Alerts(alerts...) {
for _, a := range typedAlerts {
alert := Alert{
Status: string(a.Status()),
Labels: make(KV, len(a.Labels)),
Expand Down
Loading