From b7e682dec2ea8b3e3886f6dbcca77b5b6e1e99dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ferdinand=20M=C3=BCtsch?= Date: Sun, 21 Aug 2022 12:54:14 +0200 Subject: [PATCH] feat: grafana inlet (resolve #45) --- README.md | 1 + config/version.txt | 2 +- go.mod | 2 +- go.sum | 2 + inlets/README.md | 7 +++ inlets/alertmanager/webhook.go | 5 -- inlets/grafana/model.go | 33 ++++++++++++ inlets/grafana/webhook.go | 97 ++++++++++++++++++++++++++++++++++ main.go | 2 + 9 files changed, 144 insertions(+), 7 deletions(-) create mode 100644 inlets/grafana/model.go create mode 100644 inlets/grafana/webhook.go diff --git a/README.md b/README.md index 7eef80b..fae8a0d 100644 --- a/README.md +++ b/README.md @@ -147,6 +147,7 @@ Following inlets are currently available: |----------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------| | `default` | Simply passes the request through without any changes | ✅ | | `alertmanager` | Consumes [Alertmanager webhook requests](https://prometheus.io/docs/alerting/configuration/#webhook_config) | ✅ | +| `grafana` | Consumes [Grafana webhook requests](https://grafana.com/docs/grafana/latest/alerting/contact-points/notifiers/webhook-notifier/) | ✅ | | `webmentionio` | Accepts [Webmention.io](https://webmention.io/) webhook requests to notify about a new Webmention of one of your articles | ✅ | | `bitbucket` | Accepts [Bitbucket webhook requests](https://confluence.atlassian.com/bitbucket/tutorial-create-and-trigger-a-webhook-747606432.html) to notify about a pipeline status change | ⏳ | diff --git a/config/version.txt b/config/version.txt index a4f52a5..0fa4ae4 100644 --- a/config/version.txt +++ b/config/version.txt @@ -1 +1 @@ -3.2.0 \ No newline at end of file +3.3.0 \ No newline at end of file diff --git a/go.mod b/go.mod index cb02f21..81f9a10 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,6 @@ require ( github.com/prometheus/client_model v0.2.0 // indirect github.com/prometheus/common v0.37.0 // indirect github.com/prometheus/procfs v0.8.0 // indirect - golang.org/x/sys v0.0.0-20220804214406-8e32c043e418 // indirect + golang.org/x/sys v0.0.0-20220818161305-2296e01440c6 // indirect google.golang.org/protobuf v1.28.1 // indirect ) diff --git a/go.sum b/go.sum index ae9dc85..ac6f039 100644 --- a/go.sum +++ b/go.sum @@ -357,6 +357,8 @@ golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b h1:2n253B2r0pYSmEV+UNCQoPfU/ golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220804214406-8e32c043e418 h1:9vYwv7OjYaky/tlAeD7C4oC9EsPTlaFl1H2jS++V+ME= golang.org/x/sys v0.0.0-20220804214406-8e32c043e418/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220818161305-2296e01440c6 h1:Sx/u41w+OwrInGdEckYmEuU5gHoGSL4QbDz3S9s6j4U= +golang.org/x/sys v0.0.0-20220818161305-2296e01440c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/inlets/README.md b/inlets/README.md index a93d5d8..e3ff669 100644 --- a/inlets/README.md +++ b/inlets/README.md @@ -69,6 +69,13 @@ receivers: - url: 'http://localhost:8080/api/inlets/alertmanager_webhook/5hd9mx' ``` +## `grafana` +`/api/inlets/grafana/` + +Accepts, transforms and forwards alerts sent by [Grafana](https://grafana.com/docs/grafana/latest/alerting/) to a Telegram chat. + +Create a new contact point with `POST` method and URL `https://telepush.dev/api/inlets/grafana/`. Also see [webhook-notifier](https://grafana.com/docs/grafana/latest/alerting/contact-points/notifiers/webhook-notifier/). + ## `bitbucket` `/api/inlets/bitbucket/` diff --git a/inlets/alertmanager/webhook.go b/inlets/alertmanager/webhook.go index b8d8599..593270c 100644 --- a/inlets/alertmanager/webhook.go +++ b/inlets/alertmanager/webhook.go @@ -5,7 +5,6 @@ import ( "encoding/json" "fmt" "net/http" - "regexp" "strings" "github.com/muety/telepush/config" @@ -15,10 +14,6 @@ import ( "github.com/muety/telepush/util" ) -var ( - tokenRegex = regexp.MustCompile("^Bearer (.+)$") -) - type AlertmanagerInlet struct{} func New() inlets.Inlet { diff --git a/inlets/grafana/model.go b/inlets/grafana/model.go new file mode 100644 index 0000000..3079e45 --- /dev/null +++ b/inlets/grafana/model.go @@ -0,0 +1,33 @@ +package alertmanager + +import "time" + +// See https://grafana.com/docs/grafana/latest/alerting/contact-points/notifiers/webhook-notifier/ + +type Message struct { + Receiver string `json:"receiver"` + Status string `json:"status"` + OrgId int `json:"orgId"` + Alerts []*Alert `json:"alerts"` + ExternalURL string `json:"externalURL"` + Version string `json:"version"` + GroupKey string `json:"groupKey"` + Title string `json:"title"` + State string `json:"state"` + Message string `json:"message"` +} + +type Alert struct { + Status string `json:"status"` + Url string `json:"url"` + Labels map[string]string `json:"labels"` + Annotations map[string]string `json:"annotations"` + StartsAt time.Time `json:"startsAt"` + EndsAt time.Time `json:"endsAt"` + GeneratorURL string `json:"generatorURL"` + SilenceURL string `json:"silenceURL"` + DashboardURL string `json:"dashboardURL"` + PanelURL string `json:"panelURL"` + Fingerprint string `json:"fingerprint"` + ValueString string `json:"valueString"` +} diff --git a/inlets/grafana/webhook.go b/inlets/grafana/webhook.go new file mode 100644 index 0000000..79535e9 --- /dev/null +++ b/inlets/grafana/webhook.go @@ -0,0 +1,97 @@ +package alertmanager + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "strings" + + "github.com/muety/telepush/config" + "github.com/muety/telepush/inlets" + "github.com/muety/telepush/model" + "github.com/muety/telepush/resolvers" + "github.com/muety/telepush/util" +) + +type GrafanaInlet struct{} + +func New() inlets.Inlet { + return &GrafanaInlet{} +} + +func (i *GrafanaInlet) Handler(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var m Message + + dec := json.NewDecoder(r.Body) + if err := dec.Decode(&m); err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(err.Error())) + return + } + + message := transformMessage(&m) + + ctx := r.Context() + ctx = context.WithValue(ctx, config.KeyMessage, message) + ctx = context.WithValue(ctx, config.KeyParams, &model.MessageParams{DisableLinkPreviews: true}) + + h.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +func transformMessage(in *Message) *model.DefaultMessage { + var sb strings.Builder + sb.WriteString("*Grafana* wrote:\n\n") + + for i, a := range in.Alerts { + // Status + var statusEmoji string + switch a.Status { + case "firing": + statusEmoji = "❗️" + break + case "resolved": + statusEmoji = "✅" + } + sb.WriteString(fmt.Sprintf("*⌛️ Status:* %s %s\n", a.Status, statusEmoji)) + + // Source URL + sb.WriteString(fmt.Sprintf("*🔗 Source*: [Link](%s)\n", a.Url)) + + // Labels + if len(a.Labels) > 0 { + sb.WriteString(fmt.Sprintf("*🏷 Labels:*\n")) + for k, v := range a.Labels { + k = util.EscapeMarkdown(k) + v = util.EscapeMarkdown(v) + sb.WriteString(fmt.Sprintf("– `%s` = `%s`\n", k, v)) + } + } + + // Annotations + if len(a.Annotations) > 0 { + sb.WriteString(fmt.Sprintf("*📝 Annotations:*\n")) + for k, v := range a.Annotations { + k = util.EscapeMarkdown(k) + v = util.EscapeMarkdown(v) + sb.WriteString(fmt.Sprintf("– `%s` = `%s`\n", k, v)) + } + } + + if a.ValueString != "" { + sb.WriteString(fmt.Sprintf("*📌 Value String:*\n")) + sb.WriteString(fmt.Sprintf("`%s`\n", a.ValueString)) + } + + if i < len(in.Alerts)-1 { + sb.WriteString("---\n\n") + } + } + + return &model.DefaultMessage{ + Text: sb.String(), + Type: resolvers.TextType, + } +} diff --git a/main.go b/main.go index 8805dd0..a28c119 100644 --- a/main.go +++ b/main.go @@ -6,6 +6,7 @@ import ( "github.com/muety/telepush/handlers" alertmanagerIn "github.com/muety/telepush/inlets/alertmanager" bitbucketIn "github.com/muety/telepush/inlets/bitbucket" + grafanaIn "github.com/muety/telepush/inlets/grafana" webmentionioIn "github.com/muety/telepush/inlets/webmentionio" "github.com/muety/telepush/services" "github.com/muety/telepush/util" @@ -126,6 +127,7 @@ func main() { apiRouter.Methods(http.MethodGet, http.MethodPost).Path("/messages/{recipient}").Handler(messageChain.Append(defaultIn.New().Handler).Then(messageHandler)) apiRouter.Methods(http.MethodGet, http.MethodPost).Path("/inlets/default/{recipient}").Handler(messageChain.Append(defaultIn.New().Handler).Then(messageHandler)) apiRouter.Methods(http.MethodGet, http.MethodPost).Path("/inlets/alertmanager/{recipient}").Handler(messageChain.Append(alertmanagerIn.New().Handler).Then(messageHandler)) + apiRouter.Methods(http.MethodGet, http.MethodPost).Path("/inlets/grafana/{recipient}").Handler(messageChain.Append(grafanaIn.New().Handler).Then(messageHandler)) apiRouter.Methods(http.MethodGet, http.MethodPost).Path("/inlets/bitbucket/{recipient}").Handler(messageChain.Append(bitbucketIn.New().Handler).Then(messageHandler)) apiRouter.Methods(http.MethodGet, http.MethodPost).Path("/inlets/webmentionio/{recipient}").Handler(messageChain.Append(webmentionioIn.New().Handler).Then(messageHandler))