Skip to content

Commit b4ea4f1

Browse files
committed
Initial commit
0 parents commit b4ea4f1

File tree

8 files changed

+1427
-0
lines changed

8 files changed

+1427
-0
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
alertmanager-webhook-fcm
2+
serviceAccountKey.json

LICENSE

Lines changed: 674 additions & 0 deletions
Large diffs are not rendered by default.

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# alertmanager-webhook-fcm
2+
Prometheus Alertmanager receiver that sends messages via Firebase Cloud Messaging

fcm.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"strings"
7+
tmpltext "text/template"
8+
9+
"github.com/prometheus/alertmanager/template"
10+
"golang.org/x/net/context"
11+
12+
firebase "firebase.google.com/go"
13+
"firebase.google.com/go/messaging"
14+
)
15+
16+
const (
17+
titleTemplate = `{{- if eq .Status "resolved"}}[Resolved] {{ end -}}
18+
{{ .CommonLabels.job }} {{ .CommonLabels.alertname }}`
19+
bodyTemplate = `{{- $lastsummary := "" -}}
20+
{{- range $alert := .Alerts -}}
21+
{{- if not (eq $lastsummary $alert.Annotations.summary) }}
22+
- {{ $alert.Annotations.summary -}}
23+
{{- $lastsummary = $alert.Annotations.summary -}}
24+
{{- end -}}
25+
{{- end }}`
26+
)
27+
28+
var (
29+
tmplTitle *tmpltext.Template
30+
tmplBody *tmpltext.Template
31+
)
32+
33+
func init() {
34+
tmplTitle = tmpltext.Must(tmpltext.New("title").Option("missingkey=zero").Parse(titleTemplate))
35+
tmplBody = tmpltext.Must(tmpltext.New("body").Option("missingkey=zero").Parse(bodyTemplate))
36+
}
37+
38+
type TemplateError struct {
39+
Type string
40+
Err error
41+
}
42+
43+
func (e *TemplateError) Error() string {
44+
return fmt.Sprintf("%s expansion failed: %v", strings.Title(e.Type), e.Err)
45+
}
46+
47+
// NewMessaging returns a messaging client
48+
func NewMessaging() (*messaging.Client, error) {
49+
ctx := context.Background()
50+
app, err := firebase.NewApp(context.Background(), nil)
51+
if err != nil {
52+
return nil, err
53+
}
54+
55+
client, err := app.Messaging(ctx)
56+
if err != nil {
57+
return nil, err
58+
}
59+
return client, err
60+
}
61+
62+
// NewMessage returns a new message struct
63+
func NewMessage(title, body string) *messaging.Message {
64+
return &messaging.Message{
65+
Notification: &messaging.Notification{
66+
Title: title,
67+
Body: body,
68+
},
69+
Data: map[string]string{
70+
"title": title,
71+
"body": body,
72+
"click_action": "FLUTTER_NOTIFICATION_CLICK",
73+
},
74+
Topic: "all",
75+
// https://firebase.google.com/docs/cloud-messaging/concept-options#setting-the-priority-of-a-message
76+
Android: &messaging.AndroidConfig{
77+
Priority: "high",
78+
},
79+
}
80+
}
81+
82+
func NewMessageFromAlertmanagerData(m *template.Data) (*messaging.Message, error) {
83+
title, err := tmpltextExecuteToString(tmplTitle, m)
84+
if err != nil {
85+
return nil, &TemplateError{Type: "title", Err: err}
86+
}
87+
88+
body, err := tmpltextExecuteToString(tmplBody, m)
89+
if err != nil {
90+
return nil, &TemplateError{Type: "body", Err: err}
91+
}
92+
93+
return NewMessage(title, body), nil
94+
}
95+
96+
func tmpltextExecuteToString(tmpl *tmpltext.Template, data interface{}) (string, error) {
97+
var buff bytes.Buffer
98+
if err := tmpl.Execute(&buff, data); err != nil {
99+
return "", err
100+
}
101+
return buff.String(), nil
102+
}

go.mod

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
module github.com/jayme-github/alertmanager-webhook-fcm
2+
3+
go 1.14
4+
5+
require (
6+
cloud.google.com/go/firestore v1.2.0 // indirect
7+
firebase.google.com/go v3.13.0+incompatible
8+
github.com/prometheus/alertmanager v0.20.0
9+
github.com/prometheus/client_golang v1.6.0
10+
golang.org/x/net v0.0.0-20200602114024-627f9648deb9
11+
google.golang.org/api v0.26.0 // indirect
12+
)

go.sum

Lines changed: 494 additions & 0 deletions
Large diffs are not rendered by default.

handler.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"log"
8+
"net/http"
9+
"strconv"
10+
11+
"github.com/prometheus/alertmanager/template"
12+
)
13+
14+
// JSONResponse is the standard response struct
15+
type JSONResponse struct {
16+
Status int
17+
Message string
18+
}
19+
20+
func genericHandler(w http.ResponseWriter, req *http.Request) {
21+
// TODO: Allow to post title and message
22+
message := NewMessage("Test message", "A test message body")
23+
msg, err := fcmClient.Send(req.Context(), message)
24+
if err != nil {
25+
fcmErrors.Inc()
26+
sendJSONResponse(w, req, http.StatusInternalServerError, err.Error())
27+
return
28+
}
29+
sendJSONResponse(w, req, http.StatusOK, fmt.Sprintf("Message delivered: %s", msg))
30+
}
31+
32+
func alertHandler(w http.ResponseWriter, req *http.Request) {
33+
templateData, err := readRequestBody(req)
34+
if err != nil {
35+
log.Printf("Error parsing request body: %v", err)
36+
sendJSONResponse(w, req, http.StatusBadRequest, err.Error())
37+
return
38+
}
39+
40+
msg, err := processFcmMessage(req.Context(), templateData)
41+
42+
if err != nil {
43+
log.Printf("Error sending fcm message: %v", err)
44+
sendJSONResponse(w, req, http.StatusInternalServerError, err.Error())
45+
return
46+
}
47+
48+
sendJSONResponse(w, req, http.StatusOK, fmt.Sprintf("Message delivered: %s", msg))
49+
}
50+
51+
func readRequestBody(req *http.Request) (*template.Data, error) {
52+
defer req.Body.Close()
53+
data := template.Data{}
54+
err := json.NewDecoder(req.Body).Decode(&data)
55+
return &data, err
56+
}
57+
58+
func processFcmMessage(ctx context.Context, m *template.Data) (string, error) {
59+
message, err := NewMessageFromAlertmanagerData(m)
60+
if err != nil {
61+
te, _ := err.(*TemplateError)
62+
templateErrors.WithLabelValues(te.Type).Inc()
63+
return "", err
64+
}
65+
66+
// Send a message to the devices subscribed to the topic.
67+
fcmResponse, err := fcmClient.Send(ctx, message)
68+
if err != nil {
69+
fcmErrors.Inc()
70+
}
71+
return fcmResponse, err
72+
}
73+
74+
func sendJSONResponse(w http.ResponseWriter, req *http.Request, statusCode int, message string) {
75+
webhookRequests.WithLabelValues(req.Method, req.URL.Path, strconv.Itoa(statusCode)).Inc()
76+
77+
response, _ := json.Marshal(JSONResponse{
78+
Status: statusCode,
79+
Message: message,
80+
})
81+
w.WriteHeader(statusCode)
82+
_, err := w.Write(response)
83+
84+
if err != nil {
85+
log.Printf("Error sending response to client: %v", err)
86+
}
87+
}

main.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package main
2+
3+
import (
4+
"flag"
5+
"log"
6+
"net/http"
7+
8+
"firebase.google.com/go/messaging"
9+
"github.com/prometheus/client_golang/prometheus"
10+
"github.com/prometheus/client_golang/prometheus/promauto"
11+
"github.com/prometheus/client_golang/prometheus/promhttp"
12+
)
13+
14+
var (
15+
flagListen = flag.String("listen", ":9716", "[ip]:port to listen on for HTTP")
16+
fcmClient *messaging.Client
17+
18+
webhookRequests = promauto.NewCounterVec(
19+
prometheus.CounterOpts{
20+
Name: "alertmanager_webhook_requests_total",
21+
Help: "Total number of HTTP requests.",
22+
},
23+
[]string{"method", "handler", "status"},
24+
)
25+
templateErrors = promauto.NewCounterVec(
26+
prometheus.CounterOpts{
27+
Name: "alertmanager_webhook_template_errors_total",
28+
Help: "Total number of errors executing message templates.",
29+
},
30+
[]string{"type"},
31+
)
32+
fcmErrors = promauto.NewCounter(
33+
prometheus.CounterOpts{
34+
Name: "alertmanager_webhook_fcm_errors_total",
35+
Help: "Total number of errors talking to Firebase Cloud Messaging API.",
36+
},
37+
)
38+
)
39+
40+
func main() {
41+
var err error
42+
flag.Parse()
43+
44+
fcmClient, err = NewMessaging()
45+
if err != nil {
46+
log.Fatalf("error getting Messaging client. Do you have GOOGLE_APPLICATION_CREDENTIALS set?: %v\n", err)
47+
}
48+
49+
http.HandleFunc("/alert", alertHandler)
50+
http.HandleFunc("/generic", genericHandler)
51+
http.Handle("/metrics", promhttp.Handler())
52+
log.Printf("Listeing on: %s\n", *flagListen)
53+
log.Fatal(http.ListenAndServe(*flagListen, nil))
54+
}

0 commit comments

Comments
 (0)