From 9d89f57017b4b2a19bcc7ec86c93e6b97a316cc8 Mon Sep 17 00:00:00 2001 From: Techno Freak Date: Sat, 26 Mar 2022 17:55:18 +0300 Subject: [PATCH] feat: list silences --- README.md | 7 ++++--- grafana.go | 7 +++++++ main.go | 24 +++++++++++++++++++++++- types.go | 34 ++++++++++++++++++++++++++++++++++ 4 files changed, 68 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 03833c6..3c3433d 100644 --- a/README.md +++ b/README.md @@ -5,11 +5,12 @@ grafana-interacter is a tool to interact with your Grafana instance via a Telegram bot. Here's the list of currently supported commands: - `/render [] ` - renders the panel and sends it as image. If there are multiple panels with the same name (for example, you have a `dashboard1` and `dashboard2` both containing panel with name `panel`), it will render the first panel it will find. For specifying it, you may add the dashboard name as a prefix to your query (like `/render dashboard1 panel`). You can also provide options in a `key=value` format, which will be internally passed to a `/render` query to Grafana. Some examples are `from`, `to`, `width`, `height` (the command would look something like `/render from=now-14d to=now-7d width=100 height=100 dashboard1 panel`). By default, the params are: `width=1000&height=500&from=now-30m&to=now&tz=Europe/Moscow`. -- `/dashboards` - will list Grafana dashboards and links to them -- `/dashboard ` - will return a link to a dashboard and its panels -- `/datasources` - will return Grafana datasources +- `/dashboards` - will list Grafana dashboards and links to them. +- `/dashboard ` - will return a link to a dashboard and its panels. +- `/datasources` - will return Grafana datasources. - `/alerts` - will list both Grafana alerts and Prometheus alerts from all Prometheus datasources, if any - `/silence ` - creates a silence for Grafana alert. You need to pass a duration (like `/silence 2h test alert`) and some params for matching alerts to silence. You may use `=` for matching the value exactly (example: `/silence 2h host=localhost`), `!=` for matching everything except this value (example: `/silence 2h host!=localhost`), `=~` for matching everything that matches the regexp (example: `/silence 2h host=~local`), , `!~` for matching everything that doesn't the regexp (example: `/silence 2h host!~local`), or just provide a string that will be treated as an alert name (example: `/silence 2h test alert`). +- `/silences` - list silences (both active and expired). ## How can I set it up? diff --git a/grafana.go b/grafana.go index 9d0ad92..efd290d 100644 --- a/grafana.go +++ b/grafana.go @@ -206,6 +206,13 @@ func (g *GrafanaStruct) CreateSilence(silence Silence) error { return err } +func (g *GrafanaStruct) GetSilences() ([]Silence, error) { + silences := []Silence{} + url := g.RelativeLink("/api/alertmanager/grafana/api/v2/silences") + err := g.QueryAndDecode(url, &silences) + return silences, err +} + func (g *GrafanaStruct) Query(url string) (io.ReadCloser, error) { client := &http.Client{} req, _ := http.NewRequest("GET", url, nil) diff --git a/main.go b/main.go index c256dc8..9348c3b 100644 --- a/main.go +++ b/main.go @@ -80,6 +80,7 @@ func Execute(cmd *cobra.Command, args []string) { b.Handle("/datasources", HandleListDatasources) b.Handle("/alerts", HandleListAlerts) b.Handle("/alert", HandleSingleAlert) + b.Handle("/silences", HandleListSilences) b.Handle("/silence", HandleNewSilence) log.Info().Msg("Telegram bot listening") @@ -301,7 +302,28 @@ func HandleNewSilence(c tele.Context) error { return c.Reply(fmt.Sprintf("Error creating silence: %s", silenceErr)) } - return c.Reply("Silence created.") + return BotReply(c, "Silence created.") +} + +func HandleListSilences(c tele.Context) error { + log.Info(). + Str("sender", c.Sender().Username). + Str("text", c.Text()). + Msg("Got list silence query") + + silences, err := Grafana.GetSilences() + if err != nil { + return c.Reply(fmt.Sprintf("Error listing silence: %s", err)) + } + + var sb strings.Builder + sb.WriteString(fmt.Sprintf("Silences\n")) + + for _, silence := range silences { + sb.WriteString(silence.Serialize() + "\n\n") + } + + return BotReply(c, sb.String()) } func main() { diff --git a/types.go b/types.go index 6da1142..4b3574a 100644 --- a/types.go +++ b/types.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "strings" "time" ) @@ -103,6 +104,7 @@ type Silence struct { EndsAt time.Time `json:"endsAt"` ID string `json:"id,omitempty"` Matchers []SilenceMatcher `json:"matchers"` + Status SilenceStatus `json:"status,omitempty"` } type SilenceMatcher struct { @@ -112,6 +114,10 @@ type SilenceMatcher struct { Value string `json:"value"` } +type SilenceStatus struct { + State string `json:"state"` +} + func (rule *GrafanaAlertRule) Serialize(groupName string) string { return fmt.Sprintf("- %s %s -> %s\n", GetEmojiByStatus(rule.State), groupName, rule.Name) } @@ -119,3 +125,31 @@ func (rule *GrafanaAlertRule) Serialize(groupName string) string { func (alert *GrafanaAlert) Serialize() string { return fmt.Sprintf("- %s
%s
", GetEmojiByStatus(alert.State), SerializeAlertLabels(alert.Labels)) } + +func (silence *Silence) Serialize() string { + var sb strings.Builder + sb.WriteString(fmt.Sprintf("Starts at:
%s
\n", silence.StartsAt.String())) + sb.WriteString(fmt.Sprintf("Ends at:
%s
\n", silence.EndsAt.String())) + sb.WriteString(fmt.Sprintf("Created by:
%s
\n", silence.CreatedBy)) + sb.WriteString(fmt.Sprintf("Comment:
%s
\n", silence.Comment)) + sb.WriteString(fmt.Sprintf("Status:
%s
\n", silence.Status.State)) + sb.WriteString("Matchers: ") + + for _, matcher := range silence.Matchers { + sb.WriteString("
" + matcher.Serialize() + "
") + } + + return sb.String() +} + +func (matcher *SilenceMatcher) Serialize() string { + if matcher.IsEqual && matcher.IsRegex { + return fmt.Sprintf("%s ~= %s", matcher.Name, matcher.Value) + } else if matcher.IsEqual && !matcher.IsRegex { + return fmt.Sprintf("%s = %s", matcher.Name, matcher.Value) + } else if !matcher.IsEqual && matcher.IsRegex { + return fmt.Sprintf("%s !~ %s", matcher.Name, matcher.Value) + } else { + return fmt.Sprintf("%s != %s", matcher.Name, matcher.Value) + } +}