Help us understand the problem. What is going on with this article?

AlertmanagerのSilence作成をslackに通知する

More than 1 year has passed since last update.

最近PrometheusのAlertmanagerを使う機会があったので、ちょっとしたツールを作ったのでメモ。

何が作りたかったか

AlertmanagerではSilenceを登録することで、発生したアラートの通知有無を制御することができる。
SilenceはActive, Pending, Expiredの3つのステータスを持っていて、指定時間が経過すると勝手にステータスが遷移する。
しかし、ステータス遷移は都度確認しないとわからないし、Expiredされて数時間経ったSilenceはGCにより削除されてしまうため、履歴も追えない。
これだとオペレーションを後追いできず、若干不便なので、Silenceのステータス変更をslackに通知するツールを作りたい。

実行

内容はシンプルで、Alertmanager APIで定期的にSilenceを取得し、前回取得時との差分をSlackに通知しているだけ。

https://github.com/m-masataka/alertmanager-silence-notifier/

APIでの取得先hostとかportとかを指定すればOK

./notifier \ 
 --alertmanager.host=localhost \
 --alertmanager.port=9093 \
 --slack.username=Bot \
 --slack.channel=general \
 --slack.token={your webhook token} \
 --interval=10s \
 --timerange=5m
  • 一定間隔でAPIからSilenceを取得
  • 前回取得との差分を抽出
  • slackに出力

これを繰り返す感じ。
APIでsilenceをいい感じに取得するライブラリは本家alertmanagerに用意されているし、
slackのwebhookにメッセージを投げるライブラリも既に存在した。go-slack-webhook
なので、ほとんど手作りの物はなくいい感じ。

↓のように通知される。
slack_image.png

package main

import (
    "fmt"
    "os"
    "time"
    "net/url"
    "gopkg.in/alecthomas/kingpin.v2"
    "github.com/prometheus/alertmanager/api/v2/client/silence"
    "github.com/prometheus/alertmanager/api/v2/models"
    "github.com/prometheus/alertmanager/cli"
    "github.com/ashwanthkumar/slack-go-webhook"
)

func main() {
    os.Exit(run())
}

type IdAndState struct {
    id string
    state string
}

func run() int {
    var (
        host         = kingpin.Flag("alertmanager.host", "Alertmanager host.").Default("localhost").String()
        port         = kingpin.Flag("alertmanager.port", "Alertmanager port.").Default("9093").String()
        username     = kingpin.Flag("slack.username", "username of slack bot.").Default("Bot").String()
        channel      = kingpin.Flag("slack.channel", "post channel.").Default("general").String()
        token        = kingpin.Flag("slack.token", "slack api token.").Default("xxx").String()
        interval     = kingpin.Flag("interval", "api polling interval.").Default("5s").Duration()
        timerange    = kingpin.Flag("timerange", "api polling time range.").Default("5m").Duration()
    )
    kingpin.CommandLine.GetFlag("help").Short('h')
    kingpin.Parse()

    u, _ := url.Parse("http://" + *host + ":" + *port)
    silenceParams := silence.NewGetSilencesParams()
    amclient := cli.NewAlertmanagerClient(u)

    prev := make([]IdAndState, 0)
    getOk, err := amclient.Silence.GetSilences(silenceParams)
    if err != nil {
        fmt.Println(err)
        return 1
    }
    for _, silence := range getOk.Payload {
        if time.Time(*silence.EndsAt).After(time.Now().UTC().Add(- *timerange)) {
            prev = append(prev, IdAndState{*silence.ID, *silence.Status.State})
        }
    }

    for {
        tmp := make([]IdAndState, 0)
        getOk, err = amclient.Silence.GetSilences(silenceParams)
        if err != nil {
            fmt.Println(err)
        } else {
            for _, silence := range getOk.Payload {
                if time.Time(*silence.EndsAt).After(time.Now().UTC().Add(-5 * time.Minute)) {
                    if CompareSilences(prev, *silence.ID, *silence.Status.State) {
                        PostSlack(*silence,*username,*channel,*token, *host, *port)
                    }
                    tmp = append(tmp, IdAndState{*silence.ID, *silence.Status.State})
                }
            }
            prev = tmp
        }
        time.Sleep(*interval)
    }
    return 0
}

func CompareSilences(list []IdAndState, id string, state string) bool {
    for _, v := range list {
        if v.id == id && v.state == state {
            return false
        }
    }
    return true
}

func PostSlack(s models.GettableSilence, username string, channel string, token string, host string, port string) error {
    titleStr := *s.Status.State + " : " + *s.ID
    valueStr := "Starts at: " + s.Silence.StartsAt.String() + "\n" +
        "Ends at     : " + s.Silence.EndsAt.String() + "\n" +
        "Updated at  : " + s.UpdatedAt.String() + "\n" +
        "Created by  : " + *s.Silence.CreatedBy + "\n" +
        "Comment     : " + *s.Silence.Comment + "\n" +
        "Matchers:\n"
    for _, matcher := range s.Silence.Matchers {
        var operator string
        if *matcher.IsRegex {
            operator = "~="
        } else {
            operator = "="
        }
        valueStr += *matcher.Name + operator + *matcher.Value + "\n"
    }
    silenceUrl := "http://" + host + ":" + port + "/#/silences/" + *s.ID
    attachment := slack.Attachment{}
    attachment.AddField(slack.Field{ Title: titleStr, Value: valueStr })
    attachment.AddAction(slack.Action { Type: "button", Text: "View", Url: silenceUrl })
    var color string
    var msg string
    if *s.Status.State == "active" {
        color = "good"
        msg = "New Silence!"
    } else if *s.Status.State == "pending" {
        color = "warning"
        msg = "New Silence!"
    } else {
        color = "danger"
        msg = "Expired Silence!"
    }
    attachment.Color = &color
    payload := slack.Payload {
        Username: username,
        Channel: channel,
        Text: msg,
        Attachments: []slack.Attachment{attachment},
    }
    err := slack.Send("https://hooks.slack.com/services/" + token, "", payload)
    if err != nil {
        return err[0]
    }
    return nil
}
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away