Skip to content

Commit

Permalink
feat: add silences count when creating silence via callback (#73)
Browse files Browse the repository at this point in the history
  • Loading branch information
freak12techno authored Dec 15, 2024
1 parent fe708f5 commit bd33a64
Show file tree
Hide file tree
Showing 14 changed files with 134 additions and 47 deletions.
2 changes: 2 additions & 0 deletions assets/responses/silence-prepare-ok.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@
key1 = value1
key2 = value2

Alerts that would match that silence: 1

Please choose for how long to mute this alert:
19 changes: 16 additions & 3 deletions pkg/app/silences_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"main/pkg/types"
"main/pkg/types/render"
"main/pkg/utils"
"main/pkg/utils/generic"
"strings"
"time"

Expand Down Expand Up @@ -63,6 +64,13 @@ func (a *App) HandlePrepareNewSilenceFromCallback(
matchers := types.QueryMatcherFromKeyValueString(labels)
matchers.Sort()

var silenceMatchers types.SilenceMatchers = generic.Map(matchers, types.MatcherFromQueryMatcher)

alerts, err := silenceManager.GetMatchingAlerts(silenceMatchers)
if err != nil {
return c.Reply(fmt.Sprintf("Could not fetch alerts matching this silence: %s", err))
}

menu := &tele.ReplyMarkup{ResizeKeyboard: true}
mutesDurations := silenceManager.GetMutesDurations()
rows := make([]tele.Row, 0)
Expand Down Expand Up @@ -90,16 +98,21 @@ func (a *App) HandlePrepareNewSilenceFromCallback(

menu.Inline(rows...)

response := types.SilencePrepareStruct{
Matchers: matchers,
AlertsCount: len(alerts),
}

if len(callbackSplit) > 1 {
return a.EditRender(c, "silence_prepare_create", render.RenderStruct{
Grafana: a.Grafana,
Data: matchers,
Data: response,
}, menu)
}

return a.ReplyRender(c, "silence_prepare_create", render.RenderStruct{
Grafana: a.Grafana,
Data: matchers,
Data: response,
}, menu)
}
}
Expand Down Expand Up @@ -162,7 +175,7 @@ func (a *App) HandleNewSilenceGeneric(
return c.Reply(fmt.Sprintf("Error getting created silence: %s", silenceErr))
}

alerts, alertsErr := silenceManager.GetSilenceMatchingAlerts(silence)
alerts, alertsErr := silenceManager.GetMatchingAlerts(silence.Matchers)
if alertsErr != nil {
return c.Reply(fmt.Sprintf("Error getting alerts for silence: %s", alertsErr))
}
Expand Down
76 changes: 76 additions & 0 deletions pkg/app/silences_create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,72 @@ func TestAppPrepareSilenceViaCallbackAlertNotFound(t *testing.T) {
require.NoError(t, err)
}

//nolint:paralleltest // disabled
func TestAppPrepareSilenceViaCallbackFailedToFetchMatchingAlerts(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()

config := &configPkg.Config{
Timezone: "Etc/GMT",
Log: configPkg.LogConfig{LogLevel: "info"},
Telegram: configPkg.TelegramConfig{Token: "xxx:yyy", Admins: []int64{1, 2}},
Grafana: configPkg.GrafanaConfig{
URL: "https://example.com",
Silences: null.BoolFrom(true),
MutesDurations: []string{"1h", "3h"},
},
Alertmanager: nil,
Prometheus: nil,
}

httpmock.RegisterResponder(
"POST",
"https://api.telegram.org/botxxx:yyy/getMe",
httpmock.NewBytesResponder(200, assets.GetBytesOrPanic("telegram-bot-ok.json")))

httpmock.RegisterResponder(
"GET",
"https://example.com/api/alertmanager/grafana/api/v2/alerts?filter=key1%3D%22value1%22&filter=key2%3D%22value2%22&silenced=true&inhibited=true&active=true",
httpmock.NewErrorResponder(errors.New("custom error")))

app := NewApp(config, "1.2.3")

queryMatchers := types.QueryMatcherFromKeyValueString("key2=value2 key1=value1")
key := app.Cache.Set(queryMatchers.GetHash(), queryMatchers.ToQueryString())

httpmock.RegisterMatcherResponder(
"POST",
"https://api.telegram.org/botxxx:yyy/sendMessage",
types.TelegramResponseHasText("Could not fetch alerts matching this silence: Get \"https://example.com/api/alertmanager/grafana/api/v2/alerts?filter=key1%3D%22value1%22&filter=key2%3D%22value2%22&silenced=true&inhibited=true&active=true\": custom error"),
httpmock.NewBytesResponder(200, assets.GetBytesOrPanic("telegram-send-message-ok.json")),
)

ctx := app.Bot.NewContext(tele.Update{
ID: 1,
Message: &tele.Message{
Sender: &tele.User{Username: "testuser"},
Text: "/grafana_silence",
Chat: &tele.Chat{ID: 2},
},
Callback: &tele.Callback{
Sender: &tele.User{Username: "testuser"},
Unique: "\f" + constants.GrafanaSilencePrefix,
Data: key,
Message: &tele.Message{
Sender: &tele.User{Username: "testuser"},
Text: "/grafana_silence",
Chat: &tele.Chat{ID: 2},
},
},
})

err := app.HandlePrepareNewSilenceFromCallback(
app.AlertSourcesWithSilenceManager[0].SilenceManager,
app.AlertSourcesWithSilenceManager[0].AlertSource,
)(ctx)
require.NoError(t, err)
}

//nolint:paralleltest // disabled
func TestAppPrepareSilenceViaCallbackOk(t *testing.T) {
httpmock.Activate()
Expand All @@ -394,6 +460,11 @@ func TestAppPrepareSilenceViaCallbackOk(t *testing.T) {
"https://api.telegram.org/botxxx:yyy/getMe",
httpmock.NewBytesResponder(200, assets.GetBytesOrPanic("telegram-bot-ok.json")))

httpmock.RegisterResponder(
"GET",
"https://example.com/api/alertmanager/grafana/api/v2/alerts?filter=key1%3D%22value1%22&filter=key2%3D%22value2%22&silenced=true&inhibited=true&active=true",
httpmock.NewBytesResponder(200, assets.GetBytesOrPanic("alertmanager-alerts.json")))

app := NewApp(config, "1.2.3")

queryMatchers := types.QueryMatcherFromKeyValueString("key2=value2 key1=value1")
Expand Down Expand Up @@ -489,6 +560,11 @@ func TestAppPrepareSilenceViaCallbackOkWithEditKeyboard(t *testing.T) {
"https://api.telegram.org/botxxx:yyy/getMe",
httpmock.NewBytesResponder(200, assets.GetBytesOrPanic("telegram-bot-ok.json")))

httpmock.RegisterResponder(
"GET",
"https://example.com/api/alertmanager/grafana/api/v2/alerts?filter=key1%3D%22value1%22&filter=key2%3D%22value2%22&silenced=true&inhibited=true&active=true",
httpmock.NewBytesResponder(200, assets.GetBytesOrPanic("alertmanager-alerts.json")))

app := NewApp(config, "1.2.3")

queryMatchers := types.QueryMatcherFromKeyValueString("key2=value2 key1=value1")
Expand Down
4 changes: 2 additions & 2 deletions pkg/silence_manager/alertmanager.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,10 @@ func (g *Alertmanager) CreateSilence(silence types.Silence) (types.SilenceCreate
return res, err
}

func (g *Alertmanager) GetSilenceMatchingAlerts(silence types.Silence) ([]types.AlertmanagerAlert, error) {
func (g *Alertmanager) GetMatchingAlerts(matchers types.SilenceMatchers) ([]types.AlertmanagerAlert, error) {
relativeUrl := fmt.Sprintf(
"/api/v2/alerts?%s&silenced=true&inhibited=true&active=true",
silence.GetFilterQueryString(),
matchers.GetFilterQueryString(),
)
url := g.RelativeLink(relativeUrl)
var res []types.AlertmanagerAlert
Expand Down
12 changes: 4 additions & 8 deletions pkg/silence_manager/alertmanager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,10 +191,8 @@ func TestAlertmanagerListSilenceAlertsFailed(t *testing.T) {
"https://example.com/api/v2/alerts?filter=key%3D%22value%22&silenced=true&inhibited=true&active=true",
httpmock.NewErrorResponder(errors.New("custom error")))

alerts, err := client.GetSilenceMatchingAlerts(types.Silence{
Matchers: types.SilenceMatchers{
{IsEqual: true, IsRegex: false, Name: "key", Value: "value"},
},
alerts, err := client.GetMatchingAlerts(types.SilenceMatchers{
{IsEqual: true, IsRegex: false, Name: "key", Value: "value"},
})
require.Error(t, err)
require.ErrorContains(t, err, "custom error")
Expand All @@ -215,10 +213,8 @@ func TestAlertmanagerListSilenceAlertsOk(t *testing.T) {
"https://example.com/api/v2/alerts?filter=key%3D%22value%22&silenced=true&inhibited=true&active=true",
httpmock.NewBytesResponder(200, assets.GetBytesOrPanic("alertmanager-alerts.json")))

alerts, err := client.GetSilenceMatchingAlerts(types.Silence{
Matchers: types.SilenceMatchers{
{IsEqual: true, IsRegex: false, Name: "key", Value: "value"},
},
alerts, err := client.GetMatchingAlerts(types.SilenceMatchers{
{IsEqual: true, IsRegex: false, Name: "key", Value: "value"},
})
require.NoError(t, err)
require.NotEmpty(t, alerts)
Expand Down
4 changes: 2 additions & 2 deletions pkg/silence_manager/grafana.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,10 @@ func (g *Grafana) DeleteSilence(silenceID string) error {
return g.Client.Delete(url, g.GetAuth())
}

func (g *Grafana) GetSilenceMatchingAlerts(silence types.Silence) ([]types.AlertmanagerAlert, error) {
func (g *Grafana) GetMatchingAlerts(matchers types.SilenceMatchers) ([]types.AlertmanagerAlert, error) {
relativeUrl := fmt.Sprintf(
"/api/alertmanager/grafana/api/v2/alerts?%s&silenced=true&inhibited=true&active=true",
silence.GetFilterQueryString(),
matchers.GetFilterQueryString(),
)
url := g.RelativeLink(relativeUrl)
var res []types.AlertmanagerAlert
Expand Down
12 changes: 4 additions & 8 deletions pkg/silence_manager/grafana_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,10 +196,8 @@ func TestGrafanaListSilenceAlertsFailed(t *testing.T) {
"https://example.com/api/alertmanager/grafana/api/v2/alerts?filter=key%3D%22value%22&silenced=true&inhibited=true&active=true",
httpmock.NewErrorResponder(errors.New("custom error")))

alerts, err := client.GetSilenceMatchingAlerts(types.Silence{
Matchers: types.SilenceMatchers{
{IsEqual: true, IsRegex: false, Name: "key", Value: "value"},
},
alerts, err := client.GetMatchingAlerts(types.SilenceMatchers{
{IsEqual: true, IsRegex: false, Name: "key", Value: "value"},
})
require.Error(t, err)
require.ErrorContains(t, err, "custom error")
Expand All @@ -220,10 +218,8 @@ func TestGrafanaListSilenceAlertsOk(t *testing.T) {
"https://example.com/api/alertmanager/grafana/api/v2/alerts?filter=key%3D%22value%22&silenced=true&inhibited=true&active=true",
httpmock.NewBytesResponder(200, assets.GetBytesOrPanic("alertmanager-alerts.json")))

alerts, err := client.GetSilenceMatchingAlerts(types.Silence{
Matchers: types.SilenceMatchers{
{IsEqual: true, IsRegex: false, Name: "key", Value: "value"},
},
alerts, err := client.GetMatchingAlerts(types.SilenceMatchers{
{IsEqual: true, IsRegex: false, Name: "key", Value: "value"},
})
require.NoError(t, err)
require.NotEmpty(t, alerts)
Expand Down
4 changes: 2 additions & 2 deletions pkg/silence_manager/silence_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ type SilenceManager interface {
GetSilences() (types.Silences, error)
GetSilence(silenceID string) (types.Silence, error)
CreateSilence(silence types.Silence) (types.SilenceCreateResponse, error)
GetSilenceMatchingAlerts(silence types.Silence) ([]types.AlertmanagerAlert, error)
GetMatchingAlerts(matchers types.SilenceMatchers) ([]types.AlertmanagerAlert, error)
DeleteSilence(silenceID string) error
Prefixes() Prefixes
Name() string
Expand Down Expand Up @@ -56,7 +56,7 @@ func GetSilencesWithAlerts(
go func(index int, silence types.Silence) {
defer wg.Done()

alerts, alertsErr := manager.GetSilenceMatchingAlerts(silence)
alerts, alertsErr := manager.GetMatchingAlerts(silence.Matchers)
if alertsErr != nil {
mutex.Lock()
errs = append(errs, alertsErr)
Expand Down
2 changes: 1 addition & 1 deletion pkg/silence_manager/stub_silence_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func (m *StubSilenceManager) CreateSilence(silence types.Silence) (types.Silence
return types.SilenceCreateResponse{SilenceID: silence.ID}, nil
}

func (m *StubSilenceManager) GetSilenceMatchingAlerts(silence types.Silence) ([]types.AlertmanagerAlert, error) {
func (m *StubSilenceManager) GetMatchingAlerts(matchers types.SilenceMatchers) ([]types.AlertmanagerAlert, error) {
if m.GetSilenceMatchingAlertsError != nil {
return nil, m.GetSilenceMatchingAlertsError
}
Expand Down
5 changes: 5 additions & 0 deletions pkg/types/internal.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,8 @@ type SingleAlertStruct struct {
func (s SingleAlertStruct) GetAlertFiringFor(alert GrafanaAlert) time.Duration {
return s.RenderTime.Sub(alert.ActiveAt)
}

type SilencePrepareStruct struct {
Matchers QueryMatchers
AlertsCount int
}
24 changes: 12 additions & 12 deletions pkg/types/silence.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ func (s Silences) FindByNameOrMatchers(source string) (*Silence, bool) {

for index, queryMatcher := range queryMatchers {
silenceMatcher := MatcherFromQueryMatcher(queryMatcher)
silenceMatchers[index] = *silenceMatcher
silenceMatchers[index] = silenceMatcher
}

silenceFound, found := generic.Find(s, func(s Silence) bool {
Expand All @@ -44,15 +44,7 @@ type Silence struct {
Status SilenceStatus `json:"status,omitempty"`
}

func (s Silence) GetFilterQueryString() string {
filtersParts := generic.Map(s.Matchers, func(m SilenceMatcher) string {
return "filter=" + url.QueryEscape(m.SerializeQueryString())
})

return strings.Join(filtersParts, "&")
}

type SilenceMatchers []SilenceMatcher
type SilenceMatchers []*SilenceMatcher

type SilenceMatcher struct {
IsEqual bool `json:"isEqual"`
Expand Down Expand Up @@ -98,8 +90,8 @@ func (matchers SilenceMatchers) Equals(otherMatchers SilenceMatchers) bool {
}

for _, matcher := range matchers {
_, found := generic.Find(otherMatchers, func(m SilenceMatcher) bool {
return m.Equals(&matcher)
_, found := generic.Find(otherMatchers, func(m *SilenceMatcher) bool {
return m.Equals(matcher)
})

if !found {
Expand All @@ -110,6 +102,14 @@ func (matchers SilenceMatchers) Equals(otherMatchers SilenceMatchers) bool {
return true
}

func (matchers SilenceMatchers) GetFilterQueryString() string {
filtersParts := generic.Map(matchers, func(m *SilenceMatcher) string {
return "filter=" + url.QueryEscape(m.SerializeQueryString())
})

return strings.Join(filtersParts, "&")
}

type SilenceWithAlerts struct {
Silence Silence
AlertsPresent bool
Expand Down
9 changes: 3 additions & 6 deletions pkg/types/silence_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,11 @@ func TestFindSilence(t *testing.T) {
func TestSilenceGetFilterQueryString(t *testing.T) {
t.Parallel()

silence := Silence{
ID: "silence",
Matchers: SilenceMatchers{
{IsEqual: true, IsRegex: false, Name: "key", Value: "value"},
},
matchers := SilenceMatchers{
{IsEqual: true, IsRegex: false, Name: "key", Value: "value"},
}

qs := silence.GetFilterQueryString()
qs := matchers.GetFilterQueryString()
require.Equal(t, "filter=key%3D%22value%22", qs)
}

Expand Down
4 changes: 2 additions & 2 deletions pkg/utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ func ParseSilenceWithDuration(
silence := &types.Silence{
StartsAt: time.Now(),
EndsAt: time.Now().Add(duration),
Matchers: []types.SilenceMatcher{},
Matchers: types.SilenceMatchers{},
CreatedBy: sender,
Comment: fmt.Sprintf(
"Muted using grafana-interacter for %s by %s",
Expand All @@ -125,7 +125,7 @@ func ParseSilenceWithDuration(
continue
}

matcherParsed := types.SilenceMatcher{
matcherParsed := &types.SilenceMatcher{
Name: matcher.Key,
Value: matcher.Value,
}
Expand Down
4 changes: 3 additions & 1 deletion templates/silence_prepare_create.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<strong>Going to mute an alert with the following matchers:</strong>
{{- range $matcherId, $matcher := .Data }}
{{- range $matcherId, $matcher := .Data.Matchers }}
{{ $matcher.Serialize }}
{{- end }}

Alerts that would match that silence: {{ .Data.AlertsCount }}

Please choose for how long to mute this alert:

0 comments on commit bd33a64

Please sign in to comment.