Browse Source

Add bot interaction

master
Silke 3 years ago
parent
commit
77974bdd9c
  1. 2
      .gitignore
  2. 28
      alertmanager.go
  3. 22
      alertmanager_matrix.go
  4. 40
      alerts.go
  5. 99
      matrix.go
  6. 143
      matrix_room.go
  7. 12
      prometheus.go
  8. 38
      util.go

2
.gitignore

@ -1 +1 @@
alertmanager-matrix
alertmanager_matrix

28
alertmanager.go

@ -0,0 +1,28 @@
package main
import (
alertmanager "github.com/prometheus/alertmanager/client"
"github.com/prometheus/client_golang/api"
)
type amClient struct {
alert alertmanager.AlertAPI
silence alertmanager.SilenceAPI
status alertmanager.StatusAPI
}
var am amClient
func startAlertmanagerClient(url string) error {
c, err := api.NewClient(api.Config{Address: url})
if err != nil {
return err
}
am = amClient{
alert: alertmanager.NewAlertAPI(c),
silence: alertmanager.NewSilenceAPI(c),
status: alertmanager.NewStatusAPI(c),
}
return nil
}

22
alertmanager_matrix.go

@ -4,7 +4,6 @@ import (
"encoding/json"
"flag"
"github.com/gorilla/mux"
matrix "github.com/matrix-org/gomatrix"
"log"
"net/http"
"os"
@ -12,9 +11,9 @@ import (
func handler(w http.ResponseWriter, r *http.Request) {
// Get room from request
roomID := mux.Vars(r)["room"]
if roomID[0] != '!' {
log.Print("Invalid room ID: ", roomID)
room := Room{mux.Vars(r)["room"], client}
if room.ID[0] != '!' {
log.Print("Invalid room ID: ", room.ID)
w.WriteHeader(http.StatusBadRequest)
return
}
@ -29,9 +28,9 @@ func handler(w http.ResponseWriter, r *http.Request) {
// Create readable messages for Matrix
plain, html := formatAlerts(data.Alerts)
log.Printf("Sending message to %s:\n%s", roomID, plain)
log.Printf("Sending message to %s: %s", room.ID, plain)
if err := sendMessage(roomID, plain, html); err != nil {
if err := room.sendMessage(plain, html); err != nil {
log.Printf("Error sending message: %s", err)
w.WriteHeader(http.StatusInternalServerError)
}
@ -56,13 +55,14 @@ func setMapFromJsonFile(m *map[string]string, fileName string) {
func main() {
var addr string
var homeserver, userID, token, iconFile, colorFile string
var homeserver, userID, token, alertmanager, iconFile, colorFile string
var err error
flag.StringVar(&addr, "addr", ":4051", "Address to listen on.")
flag.StringVar(&homeserver, "homeserver", "https://matrix.org", "Homeserver to connect to.")
flag.StringVar(&userID, "userID", "", "User ID to connect with.")
flag.StringVar(&token, "token", "", "Token to connect with.")
flag.StringVar(&alertmanager, "alertmanager", "http://localhost:9093", "Alertmanager to connect to.")
flag.StringVar(&iconFile, "icon-file", "", "JSON file with icons for message types.")
flag.StringVar(&colorFile, "color-file", "", "JSON file with colors for message types.")
flag.Parse()
@ -82,11 +82,17 @@ func main() {
}
log.Printf("Connecting to Matrix homeserver at %s as %s", homeserver, userID)
client, err = matrix.NewClient(homeserver, userID, token)
err = startMatrixClient(homeserver, userID, token)
if err != nil {
log.Fatalf("Error connecting to Matrix: %s", err)
}
log.Printf("Connecting to Alertmanager at %s", alertmanager)
err = startAlertmanagerClient(alertmanager)
if err != nil {
log.Fatalf("Error connecting to AlertManager: %s", err)
}
r := mux.NewRouter()
r.HandleFunc("/{room}", handler).Methods("POST")
log.Print("Listening on ", addr)

40
alerts.go

@ -5,7 +5,6 @@ import (
"log"
"strings"
alertmanager "github.com/prometheus/alertmanager/client"
"github.com/prometheus/alertmanager/types"
)
@ -53,7 +52,7 @@ func createMessage(status, name, summary string) (plain, html string) {
return
}
func formatAlerts(alerts []Alert) (string, string) {
func formatAlerts(alerts []*Alert) (string, string) {
plain := make([]string, len(alerts))
html := make([]string, len(alerts))
@ -61,18 +60,20 @@ func formatAlerts(alerts []Alert) (string, string) {
status := "alert"
if a.Status == "resolved" {
status = "ok"
} else if a.Status == "suppressed" {
status = "silenced"
} else if sev, ok := a.Labels["severity"]; ok {
status = sev
status = string(sev)
}
summary := ""
if v, ok := a.Annotations["summary"]; ok {
summary = v
summary = string(v)
}
alertName := ""
if v, ok := a.Labels["alertname"]; ok {
alertName = v
alertName = string(v)
}
plain[i], html[i] = createMessage(status, alertName, summary)
@ -83,3 +84,32 @@ func formatAlerts(alerts []Alert) (string, string) {
return plainBody, htmlBody
}
func formatSilences(silences []*types.Silence) (string, string) {
plain := ""
for _, s := range silences {
if s.Status.State != "active" {
continue
}
plain += fmt.Sprintf(
"**Silence %s** \n"+
"Ends %s\n\n",
s.ID,
s.EndsAt.Format("2006-01-02 15:04"),
)
matchers := make([]string, len(s.Matchers))
for i, m := range s.Matchers {
sep := "="
if m.IsRegex {
sep = "=~"
}
matchers[i] = fmt.Sprintf("`%s%s%q`", m.Name, sep, m.Value)
}
plain += strings.Join(matchers, ", ") + "\n\n"
}
return plain, markdown(plain)
}

99
matrix.go

@ -1,9 +1,22 @@
package main
import (
"log"
"strings"
matrix "github.com/matrix-org/gomatrix"
)
const (
helpMessage = "Available commands are:\n\n" +
"- `help`: shows this message\n" +
"- `list`: shows active alerts\n" +
"- `list all`: shows active and silenced alerts\n" +
"- `silence`: shows active silences\n" +
"- `silence add <duration> <matchers>`: create a new silence\n" +
"- `silence del <ids>`: create a new silence\n"
)
type MatrixMessage struct {
MsgType string `json:"msgtype"`
Body string `json:"body"`
@ -13,15 +26,79 @@ type MatrixMessage struct {
var client *matrix.Client
func sendMessage(roomID, plain, html string) error {
_, err := client.SendMessageEvent(roomID, "m.room.message",
&MatrixMessage{
MsgType: "m.text",
Format: "org.matrix.custom.html",
Body: plain,
FormattedBody: html,
},
)
return err
// Start the Matrix client
func startMatrixClient(homeserver, userID, token string) (err error) {
// Create a new client
client, err = matrix.NewClient(homeserver, userID, token)
if err != nil {
return
}
// Create sync/message handler
syncer := client.Syncer.(*matrix.DefaultSyncer)
syncer.OnEventType("m.room.message", messageHandler)
// Start syncing
go sync()
return
}
// Sync thread
func sync() {
for {
err := client.Sync()
if err != nil {
log.Printf("Sync error: %s", err)
}
}
}
// Message handler
func messageHandler(e *matrix.Event) {
var err error
// Get message text
text, ok := e.Body()
if !ok ||
e.Sender == client.UserID ||
!strings.HasPrefix(text, "!alert") {
return
}
// Room to send response to
room := Room{e.RoomID, client}
// Get command
cmd := strings.Split(text, " ")
// Compress command
str := ""
for _, c := range cmd {
if len(c) > 0 {
str += string(c[0])
}
if len(str) == 3 {
break
}
}
switch str[1:] {
case "l":
err = room.sendAlerts(false)
case "la":
err = room.sendAlerts(true)
case "s":
err = room.sendSilences()
case "sa":
err = room.sendNewSilence(e.Sender, cmd[3:])
case "sd":
err = room.sendDelSilence(cmd[3:])
default:
err = room.sendMarkdownMessage(helpMessage)
}
if err != nil {
log.Print("Error: ", err)
}
}

143
matrix_room.go

@ -0,0 +1,143 @@
package main
import (
"context"
"fmt"
matrix "github.com/matrix-org/gomatrix"
"github.com/prometheus/alertmanager/types"
"regexp"
"strings"
"time"
)
type Room struct {
ID string
*matrix.Client
}
// Send a Markdown formatted message
func (r *Room) sendMarkdownMessage(md string) error {
return r.sendMessage(md, markdown(md))
}
// Send a plain message
func (r *Room) sendText(plain string) error {
_, err := r.SendMessageEvent(r.ID, "m.room.message",
&MatrixMessage{
MsgType: "m.notice",
Body: plain,
},
)
return err
}
// Send a formatted message to a room.
func (r *Room) sendMessage(plain, html string) error {
_, err := r.SendMessageEvent(r.ID, "m.room.message",
&MatrixMessage{
MsgType: "m.notice",
Format: "org.matrix.custom.html",
Body: plain,
FormattedBody: html,
},
)
return err
}
// sendAlerts sends alerts to a room
func (r *Room) sendAlerts(silenced bool) error {
alerts, err := am.alert.List(context.TODO(), "", silenced, false)
if err != nil {
return r.sendText(err.Error())
}
if len(alerts) == 0 {
return r.sendText("No alerts")
}
// Map alerts to compatible type
as := make([]*Alert, len(alerts))
for i, a := range alerts {
as[i] = &Alert{
Alert: a.Alert,
Status: string(a.Status.State),
}
}
plain, html := formatAlerts(as)
return r.sendMessage(plain, html)
}
// sendSilences sends silences to a room
func (r *Room) sendSilences() error {
silences, err := am.silence.List(context.TODO(), "")
if err != nil {
return r.sendText(err.Error())
}
if len(silences) == 0 {
return r.sendText("No silences")
}
plain, html := formatSilences(silences)
return r.sendMessage(plain, html)
}
// sendNewSilence creates a new silence and sends the ID
func (r *Room) sendNewSilence(author string, args []string) error {
if len(args) < 2 {
return r.sendText("Insufficent arguments")
}
matchers := args[1:]
duration, err := parseDuration(args[0])
if err != nil {
return r.sendText(err.Error())
}
silence := types.Silence{
Matchers: make(types.Matchers, len(matchers)),
StartsAt: time.Now(),
EndsAt: time.Now().Add(duration),
CreatedBy: author,
Comment: "Created from Matrix",
}
for i, m := range matchers {
ms := regexp.MustCompile(`(.*)=(~?)"(.*)"`).FindStringSubmatch(m)
if ms == nil {
return r.sendText("Invalid matcher: " + m)
}
silence.Matchers[i] = &types.Matcher{
Name: ms[1],
Value: ms[3],
IsRegex: ms[2] == "~",
}
}
id, err := am.silence.Set(context.TODO(), silence)
if err != nil {
return r.sendText(err.Error())
}
return r.sendMarkdownMessage(fmt.Sprintf("Silence created with ID *%s*", id))
}
// sendDelSilence deletes one or more silences
func (r *Room) sendDelSilence(ids []string) error {
if len(ids) == 0 {
return r.sendText("No silence IDs provided")
}
for _, id := range ids {
err := am.silence.Expire(context.TODO(), id)
if err != nil {
err = r.sendText(err.Error())
if err != nil {
return err
}
}
}
return r.sendMarkdownMessage(fmt.Sprintf(
"Silences deleted: *%s*",
strings.Join(ids, ", ")))
}

12
prometheus.go

@ -1,14 +1,12 @@
package main
import "time"
import (
alertmanager "github.com/prometheus/alertmanager/client"
)
type Alert struct {
alertmanager.Alert
Status string
GeneratorURL string
Labels map[string]string
Annotations map[string]string
StartsAt time.Time
EndsAt time.Time
}
type Message struct {
@ -20,5 +18,5 @@ type Message struct {
CommonLabels map[string]string
CommonAnnotations map[string]string
ExternalURL string
Alerts []Alert
Alerts []*Alert
}

38
util.go

@ -0,0 +1,38 @@
package main
import (
"gopkg.in/russross/blackfriday.v2"
"regexp"
"strconv"
"time"
)
var durationRegex = regexp.MustCompile(`(\d+)(\w)`)
// markdown converts Markdown to HTML
func markdown(md string) string {
return string(blackfriday.Run([]byte(md),
blackfriday.WithExtensions(blackfriday.CommonExtensions)))
}
func parseDuration(s string) (time.Duration, error) {
m := durationRegex.FindStringSubmatch(s)
if m == nil {
return time.ParseDuration(s)
}
i, err := strconv.Atoi(m[1])
if err != nil {
return time.ParseDuration(s)
}
switch m[2] {
case `d`:
return time.Duration(i*24) * time.Hour, nil
case `w`:
return time.Duration(i*24*7) * time.Hour, nil
case `y`:
return time.Duration(i*24*365) * time.Hour, nil
default:
return time.ParseDuration(s)
}
}
Loading…
Cancel
Save