mirror of
https://github.com/prometheus/alertmanager.git
synced 2026-02-05 15:45:34 +01:00
Enable the golangci-lint modernize linter. * Add exception for the omitempty struct issue. * Apply modernize fixes. Signed-off-by: SuperQ <superq@gmail.com>
554 lines
14 KiB
Go
554 lines
14 KiB
Go
// Copyright 2015 Prometheus Team
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package types //nolint:revive
|
|
|
|
import (
|
|
"reflect"
|
|
"sort"
|
|
"strconv"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
"github.com/prometheus/common/model"
|
|
"github.com/prometheus/common/promslog"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/prometheus/alertmanager/featurecontrol"
|
|
"github.com/prometheus/alertmanager/matcher/compat"
|
|
)
|
|
|
|
func TestMemMarker_Muted(t *testing.T) {
|
|
r := prometheus.NewRegistry()
|
|
marker := NewMarker(r)
|
|
|
|
// No groups should be muted.
|
|
timeIntervalNames, isMuted := marker.Muted("route1", "group1")
|
|
require.False(t, isMuted)
|
|
require.Empty(t, timeIntervalNames)
|
|
|
|
// Mark the group as muted because it's the weekend.
|
|
marker.SetMuted("route1", "group1", []string{"weekends"})
|
|
timeIntervalNames, isMuted = marker.Muted("route1", "group1")
|
|
require.True(t, isMuted)
|
|
require.Equal(t, []string{"weekends"}, timeIntervalNames)
|
|
|
|
// Other groups should not be marked as muted.
|
|
timeIntervalNames, isMuted = marker.Muted("route1", "group2")
|
|
require.False(t, isMuted)
|
|
require.Empty(t, timeIntervalNames)
|
|
|
|
// Other routes should not be marked as muted either.
|
|
timeIntervalNames, isMuted = marker.Muted("route2", "group1")
|
|
require.False(t, isMuted)
|
|
require.Empty(t, timeIntervalNames)
|
|
|
|
// The group is no longer muted.
|
|
marker.SetMuted("route1", "group1", nil)
|
|
timeIntervalNames, isMuted = marker.Muted("route1", "group1")
|
|
require.False(t, isMuted)
|
|
require.Empty(t, timeIntervalNames)
|
|
}
|
|
|
|
func TestMemMarker_DeleteByGroupKey(t *testing.T) {
|
|
r := prometheus.NewRegistry()
|
|
marker := NewMarker(r)
|
|
|
|
// Mark the group and check that it is muted.
|
|
marker.SetMuted("route1", "group1", []string{"weekends"})
|
|
timeIntervalNames, isMuted := marker.Muted("route1", "group1")
|
|
require.True(t, isMuted)
|
|
require.Equal(t, []string{"weekends"}, timeIntervalNames)
|
|
|
|
// Delete the markers for a different group key. The group should
|
|
// still be muted.
|
|
marker.DeleteByGroupKey("route1", "group2")
|
|
timeIntervalNames, isMuted = marker.Muted("route1", "group1")
|
|
require.True(t, isMuted)
|
|
require.Equal(t, []string{"weekends"}, timeIntervalNames)
|
|
|
|
// Delete the markers for the correct group key. The group should
|
|
// no longer be muted.
|
|
marker.DeleteByGroupKey("route1", "group1")
|
|
timeIntervalNames, isMuted = marker.Muted("route1", "group1")
|
|
require.False(t, isMuted)
|
|
require.Empty(t, timeIntervalNames)
|
|
}
|
|
|
|
func TestMemMarker_Count(t *testing.T) {
|
|
r := prometheus.NewRegistry()
|
|
marker := NewMarker(r)
|
|
now := time.Now()
|
|
|
|
states := []AlertState{AlertStateSuppressed, AlertStateActive, AlertStateUnprocessed}
|
|
countByState := func(state AlertState) int {
|
|
return marker.Count(state)
|
|
}
|
|
|
|
countTotal := func() int {
|
|
var count int
|
|
for _, s := range states {
|
|
count += countByState(s)
|
|
}
|
|
return count
|
|
}
|
|
|
|
require.Equal(t, 0, countTotal())
|
|
|
|
a1 := model.Alert{
|
|
StartsAt: now.Add(-2 * time.Minute),
|
|
EndsAt: now.Add(2 * time.Minute),
|
|
Labels: model.LabelSet{"test": "active"},
|
|
}
|
|
a2 := model.Alert{
|
|
StartsAt: now.Add(-2 * time.Minute),
|
|
EndsAt: now.Add(2 * time.Minute),
|
|
Labels: model.LabelSet{"test": "suppressed"},
|
|
}
|
|
a3 := model.Alert{
|
|
StartsAt: now.Add(-2 * time.Minute),
|
|
EndsAt: now.Add(-1 * time.Minute),
|
|
Labels: model.LabelSet{"test": "resolved"},
|
|
}
|
|
|
|
// Insert an active alert.
|
|
marker.SetActiveOrSilenced(a1.Fingerprint(), 1, nil, nil)
|
|
require.Equal(t, 1, countByState(AlertStateActive))
|
|
require.Equal(t, 1, countTotal())
|
|
|
|
// Insert a silenced alert.
|
|
marker.SetActiveOrSilenced(a2.Fingerprint(), 1, []string{"1"}, nil)
|
|
require.Equal(t, 1, countByState(AlertStateSuppressed))
|
|
require.Equal(t, 2, countTotal())
|
|
|
|
// Insert a resolved silenced alert - it'll count as suppressed.
|
|
marker.SetActiveOrSilenced(a3.Fingerprint(), 1, []string{"1"}, nil)
|
|
require.Equal(t, 2, countByState(AlertStateSuppressed))
|
|
require.Equal(t, 3, countTotal())
|
|
|
|
// Remove the silence from a3 - it'll count as active.
|
|
marker.SetActiveOrSilenced(a3.Fingerprint(), 1, nil, nil)
|
|
require.Equal(t, 2, countByState(AlertStateActive))
|
|
require.Equal(t, 3, countTotal())
|
|
}
|
|
|
|
func TestAlertMerge(t *testing.T) {
|
|
now := time.Now()
|
|
|
|
// By convention, alert A is always older than alert B.
|
|
pairs := []struct {
|
|
A, B, Res *Alert
|
|
}{
|
|
{
|
|
// Both alerts have the Timeout flag set.
|
|
// StartsAt is defined by Alert A.
|
|
// EndsAt is defined by Alert B.
|
|
A: &Alert{
|
|
Alert: model.Alert{
|
|
StartsAt: now.Add(-2 * time.Minute),
|
|
EndsAt: now.Add(2 * time.Minute),
|
|
},
|
|
UpdatedAt: now,
|
|
Timeout: true,
|
|
},
|
|
B: &Alert{
|
|
Alert: model.Alert{
|
|
StartsAt: now.Add(-time.Minute),
|
|
EndsAt: now.Add(3 * time.Minute),
|
|
},
|
|
UpdatedAt: now.Add(time.Minute),
|
|
Timeout: true,
|
|
},
|
|
Res: &Alert{
|
|
Alert: model.Alert{
|
|
StartsAt: now.Add(-2 * time.Minute),
|
|
EndsAt: now.Add(3 * time.Minute),
|
|
},
|
|
UpdatedAt: now.Add(time.Minute),
|
|
Timeout: true,
|
|
},
|
|
},
|
|
{
|
|
// Alert A has the Timeout flag set while Alert B has it unset.
|
|
// StartsAt is defined by Alert A.
|
|
// EndsAt is defined by Alert B.
|
|
A: &Alert{
|
|
Alert: model.Alert{
|
|
StartsAt: now.Add(-time.Minute),
|
|
EndsAt: now.Add(3 * time.Minute),
|
|
},
|
|
UpdatedAt: now,
|
|
Timeout: true,
|
|
},
|
|
B: &Alert{
|
|
Alert: model.Alert{
|
|
StartsAt: now,
|
|
EndsAt: now.Add(2 * time.Minute),
|
|
},
|
|
UpdatedAt: now.Add(time.Minute),
|
|
},
|
|
Res: &Alert{
|
|
Alert: model.Alert{
|
|
StartsAt: now.Add(-time.Minute),
|
|
EndsAt: now.Add(2 * time.Minute),
|
|
},
|
|
UpdatedAt: now.Add(time.Minute),
|
|
},
|
|
},
|
|
{
|
|
// Alert A has the Timeout flag unset while Alert B has it set.
|
|
// StartsAt is defined by Alert A.
|
|
// EndsAt is defined by Alert A.
|
|
A: &Alert{
|
|
Alert: model.Alert{
|
|
StartsAt: now.Add(-time.Minute),
|
|
EndsAt: now.Add(3 * time.Minute),
|
|
},
|
|
UpdatedAt: now,
|
|
},
|
|
B: &Alert{
|
|
Alert: model.Alert{
|
|
StartsAt: now,
|
|
EndsAt: now.Add(2 * time.Minute),
|
|
},
|
|
UpdatedAt: now.Add(time.Minute),
|
|
Timeout: true,
|
|
},
|
|
Res: &Alert{
|
|
Alert: model.Alert{
|
|
StartsAt: now.Add(-time.Minute),
|
|
EndsAt: now.Add(3 * time.Minute),
|
|
},
|
|
UpdatedAt: now.Add(time.Minute),
|
|
Timeout: true,
|
|
},
|
|
},
|
|
{
|
|
// Both alerts have the Timeout flag unset and are not resolved.
|
|
// StartsAt is defined by Alert A.
|
|
// EndsAt is defined by Alert A.
|
|
A: &Alert{
|
|
Alert: model.Alert{
|
|
StartsAt: now.Add(-time.Minute),
|
|
EndsAt: now.Add(3 * time.Minute),
|
|
},
|
|
UpdatedAt: now,
|
|
},
|
|
B: &Alert{
|
|
Alert: model.Alert{
|
|
StartsAt: now,
|
|
EndsAt: now.Add(2 * time.Minute),
|
|
},
|
|
UpdatedAt: now.Add(time.Minute),
|
|
},
|
|
Res: &Alert{
|
|
Alert: model.Alert{
|
|
StartsAt: now.Add(-time.Minute),
|
|
EndsAt: now.Add(3 * time.Minute),
|
|
},
|
|
UpdatedAt: now.Add(time.Minute),
|
|
},
|
|
},
|
|
{
|
|
// Both alerts have the Timeout flag unset and are not resolved.
|
|
// StartsAt is defined by Alert A.
|
|
// EndsAt is defined by Alert B.
|
|
A: &Alert{
|
|
Alert: model.Alert{
|
|
StartsAt: now.Add(-time.Minute),
|
|
EndsAt: now.Add(3 * time.Minute),
|
|
},
|
|
UpdatedAt: now,
|
|
},
|
|
B: &Alert{
|
|
Alert: model.Alert{
|
|
StartsAt: now.Add(-time.Minute),
|
|
EndsAt: now.Add(4 * time.Minute),
|
|
},
|
|
UpdatedAt: now.Add(time.Minute),
|
|
},
|
|
Res: &Alert{
|
|
Alert: model.Alert{
|
|
StartsAt: now.Add(-time.Minute),
|
|
EndsAt: now.Add(4 * time.Minute),
|
|
},
|
|
UpdatedAt: now.Add(time.Minute),
|
|
},
|
|
},
|
|
{
|
|
// Both alerts have the Timeout flag unset, A is resolved while B isn't.
|
|
// StartsAt is defined by Alert A.
|
|
// EndsAt is defined by Alert B.
|
|
A: &Alert{
|
|
Alert: model.Alert{
|
|
StartsAt: now.Add(-3 * time.Minute),
|
|
EndsAt: now.Add(-time.Minute),
|
|
},
|
|
UpdatedAt: now,
|
|
},
|
|
B: &Alert{
|
|
Alert: model.Alert{
|
|
StartsAt: now.Add(-2 * time.Minute),
|
|
EndsAt: now.Add(time.Minute),
|
|
},
|
|
UpdatedAt: now.Add(time.Minute),
|
|
},
|
|
Res: &Alert{
|
|
Alert: model.Alert{
|
|
StartsAt: now.Add(-3 * time.Minute),
|
|
EndsAt: now.Add(time.Minute),
|
|
},
|
|
UpdatedAt: now.Add(time.Minute),
|
|
},
|
|
},
|
|
{
|
|
// Both alerts have the Timeout flag unset, B is resolved while A isn't.
|
|
// StartsAt is defined by Alert A.
|
|
// EndsAt is defined by Alert B.
|
|
A: &Alert{
|
|
Alert: model.Alert{
|
|
StartsAt: now.Add(-2 * time.Minute),
|
|
EndsAt: now.Add(3 * time.Minute),
|
|
},
|
|
UpdatedAt: now,
|
|
},
|
|
B: &Alert{
|
|
Alert: model.Alert{
|
|
StartsAt: now.Add(-2 * time.Minute),
|
|
EndsAt: now,
|
|
},
|
|
UpdatedAt: now.Add(time.Minute),
|
|
},
|
|
Res: &Alert{
|
|
Alert: model.Alert{
|
|
StartsAt: now.Add(-2 * time.Minute),
|
|
EndsAt: now,
|
|
},
|
|
UpdatedAt: now.Add(time.Minute),
|
|
},
|
|
},
|
|
{
|
|
// Both alerts are resolved (EndsAt < now).
|
|
// StartsAt is defined by Alert B.
|
|
// EndsAt is defined by Alert A.
|
|
A: &Alert{
|
|
Alert: model.Alert{
|
|
StartsAt: now.Add(-3 * time.Minute),
|
|
EndsAt: now.Add(-time.Minute),
|
|
},
|
|
UpdatedAt: now.Add(-time.Minute),
|
|
},
|
|
B: &Alert{
|
|
Alert: model.Alert{
|
|
StartsAt: now.Add(-4 * time.Minute),
|
|
EndsAt: now.Add(-2 * time.Minute),
|
|
},
|
|
UpdatedAt: now.Add(time.Minute),
|
|
},
|
|
Res: &Alert{
|
|
Alert: model.Alert{
|
|
StartsAt: now.Add(-4 * time.Minute),
|
|
EndsAt: now.Add(-1 * time.Minute),
|
|
},
|
|
UpdatedAt: now.Add(time.Minute),
|
|
},
|
|
},
|
|
}
|
|
|
|
for i, p := range pairs {
|
|
t.Run(strconv.Itoa(i), func(t *testing.T) {
|
|
if res := p.A.Merge(p.B); !reflect.DeepEqual(p.Res, res) {
|
|
t.Errorf("unexpected merged alert %#v", res)
|
|
}
|
|
if res := p.B.Merge(p.A); !reflect.DeepEqual(p.Res, res) {
|
|
t.Errorf("unexpected merged alert %#v", res)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateUTF8Ls(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
ls model.LabelSet
|
|
err string
|
|
}{{
|
|
name: "valid UTF-8 label set",
|
|
ls: model.LabelSet{
|
|
"a": "a",
|
|
"00": "b",
|
|
"Σ": "c",
|
|
"\xf0\x9f\x99\x82": "dΘ",
|
|
},
|
|
}, {
|
|
name: "invalid UTF-8 label set",
|
|
ls: model.LabelSet{
|
|
"\xff": "a",
|
|
},
|
|
err: "invalid name \"\\xff\"",
|
|
}}
|
|
|
|
// Change the mode to UTF-8 mode.
|
|
ff, err := featurecontrol.NewFlags(promslog.NewNopLogger(), featurecontrol.FeatureUTF8StrictMode)
|
|
require.NoError(t, err)
|
|
compat.InitFromFlags(promslog.NewNopLogger(), ff)
|
|
|
|
// Restore the mode to classic at the end of the test.
|
|
ff, err = featurecontrol.NewFlags(promslog.NewNopLogger(), featurecontrol.FeatureClassicMode)
|
|
require.NoError(t, err)
|
|
defer compat.InitFromFlags(promslog.NewNopLogger(), ff)
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
err := validateLs(test.ls)
|
|
if err != nil && err.Error() != test.err {
|
|
t.Errorf("unexpected err for %s: %s", test.ls, err)
|
|
} else if err == nil && test.err != "" {
|
|
t.Error("expected error, got nil")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCalcSilenceState(t *testing.T) {
|
|
var (
|
|
pastStartTime = time.Now()
|
|
pastEndTime = time.Now()
|
|
|
|
futureStartTime = time.Now().Add(time.Hour)
|
|
futureEndTime = time.Now().Add(time.Hour)
|
|
)
|
|
|
|
expected := CalcSilenceState(futureStartTime, futureEndTime)
|
|
require.Equal(t, SilenceStatePending, expected)
|
|
|
|
expected = CalcSilenceState(pastStartTime, futureEndTime)
|
|
require.Equal(t, SilenceStateActive, expected)
|
|
|
|
expected = CalcSilenceState(pastStartTime, pastEndTime)
|
|
require.Equal(t, SilenceStateExpired, expected)
|
|
}
|
|
|
|
func TestSilenceExpired(t *testing.T) {
|
|
now := time.Now()
|
|
silence := Silence{StartsAt: now, EndsAt: now}
|
|
require.True(t, silence.Expired())
|
|
|
|
silence = Silence{StartsAt: now.Add(time.Hour), EndsAt: now.Add(time.Hour)}
|
|
require.True(t, silence.Expired())
|
|
|
|
silence = Silence{StartsAt: now, EndsAt: now.Add(time.Hour)}
|
|
require.False(t, silence.Expired())
|
|
}
|
|
|
|
func TestAlertSliceSort(t *testing.T) {
|
|
var (
|
|
a1 = &Alert{
|
|
Alert: model.Alert{
|
|
Labels: model.LabelSet{
|
|
"job": "j1",
|
|
"instance": "i1",
|
|
"alertname": "an1",
|
|
},
|
|
},
|
|
}
|
|
a2 = &Alert{
|
|
Alert: model.Alert{
|
|
Labels: model.LabelSet{
|
|
"job": "j1",
|
|
"instance": "i1",
|
|
"alertname": "an2",
|
|
},
|
|
},
|
|
}
|
|
a3 = &Alert{
|
|
Alert: model.Alert{
|
|
Labels: model.LabelSet{
|
|
"job": "j2",
|
|
"instance": "i1",
|
|
"alertname": "an1",
|
|
},
|
|
},
|
|
}
|
|
a4 = &Alert{
|
|
Alert: model.Alert{
|
|
Labels: model.LabelSet{
|
|
"alertname": "an1",
|
|
},
|
|
},
|
|
}
|
|
a5 = &Alert{
|
|
Alert: model.Alert{
|
|
Labels: model.LabelSet{
|
|
"alertname": "an2",
|
|
},
|
|
},
|
|
}
|
|
)
|
|
|
|
cases := []struct {
|
|
alerts AlertSlice
|
|
exp AlertSlice
|
|
}{
|
|
{
|
|
alerts: AlertSlice{a2, a1},
|
|
exp: AlertSlice{a1, a2},
|
|
},
|
|
{
|
|
alerts: AlertSlice{a3, a2, a1},
|
|
exp: AlertSlice{a1, a2, a3},
|
|
},
|
|
{
|
|
alerts: AlertSlice{a4, a2, a4},
|
|
exp: AlertSlice{a2, a4, a4},
|
|
},
|
|
{
|
|
alerts: AlertSlice{a5, a4},
|
|
exp: AlertSlice{a4, a5},
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
sort.Stable(tc.alerts)
|
|
if !reflect.DeepEqual(tc.alerts, tc.exp) {
|
|
t.Fatalf("expected %v but got %v", tc.exp, tc.alerts)
|
|
}
|
|
}
|
|
}
|
|
|
|
type fakeRegisterer struct {
|
|
registeredCollectors []prometheus.Collector
|
|
}
|
|
|
|
func (r *fakeRegisterer) Register(prometheus.Collector) error {
|
|
return nil
|
|
}
|
|
|
|
func (r *fakeRegisterer) MustRegister(c ...prometheus.Collector) {
|
|
r.registeredCollectors = append(r.registeredCollectors, c...)
|
|
}
|
|
|
|
func (r *fakeRegisterer) Unregister(prometheus.Collector) bool {
|
|
return false
|
|
}
|
|
|
|
func TestNewMarkerRegistersMetrics(t *testing.T) {
|
|
fr := fakeRegisterer{}
|
|
NewMarker(&fr)
|
|
|
|
if len(fr.registeredCollectors) == 0 {
|
|
t.Error("expected NewMarker to register metrics on the given registerer")
|
|
}
|
|
}
|