NGAlert: Add Threema notification channel (#34159)

Signed-off-by: Ganesh Vernekar <ganeshvern@gmail.com>
This commit is contained in:
Ganesh Vernekar 2021-05-19 23:50:52 +05:30 committed by GitHub
parent b2e84277a3
commit 533be16787
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 375 additions and 0 deletions

View File

@ -438,6 +438,8 @@ func (am *Alertmanager) buildReceiverIntegrations(receiver *apimodels.PostableAp
n, err = channels.NewGoogleChatNotifier(cfg, tmpl)
case "line":
n, err = channels.NewLineNotifier(cfg, tmpl)
case "threema":
n, err = channels.NewThreemaNotifier(cfg, tmpl)
default:
return nil, fmt.Errorf("notifier %s is not supported", r.Type)
}

View File

@ -706,5 +706,44 @@ func GetAvailableNotifiers() []*alerting.NotifierPlugin {
Secure: true,
}},
},
{
Type: "threema",
Name: "Threema Gateway",
Description: "Sends notifications to Threema using the Threema Gateway",
Heading: "Threema Gateway settings",
Info: "Notifications can be configured for any Threema Gateway ID of type \"Basic\". End-to-End IDs are not currently supported." +
"The Threema Gateway ID can be set up at https://gateway.threema.ch/.",
Options: []alerting.NotifierOption{
{
Label: "Gateway ID",
Element: alerting.ElementTypeInput,
InputType: alerting.InputTypeText,
Placeholder: "*3MAGWID",
Description: "Your 8 character Threema Gateway ID (starting with a *).",
PropertyName: "gateway_id",
Required: true,
ValidationRule: "\\*[0-9A-Z]{7}",
},
{
Label: "Recipient ID",
Element: alerting.ElementTypeInput,
InputType: alerting.InputTypeText,
Placeholder: "YOUR3MID",
Description: "The 8 character Threema ID that should receive the alerts.",
PropertyName: "recipient_id",
Required: true,
ValidationRule: "[0-9A-Z]{8}",
},
{
Label: "API Secret",
Element: alerting.ElementTypeInput,
InputType: alerting.InputTypeText,
Description: "Your Threema Gateway API secret.",
PropertyName: "api_secret",
Required: true,
Secure: true,
},
},
},
}
}

View File

@ -0,0 +1,136 @@
package channels
import (
"context"
"fmt"
"net/url"
"path"
"strings"
gokit_log "github.com/go-kit/kit/log"
"github.com/prometheus/alertmanager/notify"
"github.com/prometheus/alertmanager/template"
"github.com/prometheus/alertmanager/types"
"github.com/prometheus/common/model"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
old_notifiers "github.com/grafana/grafana/pkg/services/alerting/notifiers"
)
var (
ThreemaGwBaseURL = "https://msgapi.threema.ch/send_simple"
)
// ThreemaNotifier is responsible for sending
// alert notifications to Threema.
type ThreemaNotifier struct {
old_notifiers.NotifierBase
GatewayID string
RecipientID string
APISecret string
log log.Logger
tmpl *template.Template
}
// NewThreemaNotifier is the constructor for the Threema notifier
func NewThreemaNotifier(model *NotificationChannelConfig, t *template.Template) (*ThreemaNotifier, error) {
if model.Settings == nil {
return nil, alerting.ValidationError{Reason: "No Settings Supplied"}
}
gatewayID := model.Settings.Get("gateway_id").MustString()
recipientID := model.Settings.Get("recipient_id").MustString()
apiSecret := model.DecryptedValue("api_secret", model.Settings.Get("api_secret").MustString())
// Validation
if gatewayID == "" {
return nil, alerting.ValidationError{Reason: "Could not find Threema Gateway ID in settings"}
}
if !strings.HasPrefix(gatewayID, "*") {
return nil, alerting.ValidationError{Reason: "Invalid Threema Gateway ID: Must start with a *"}
}
if len(gatewayID) != 8 {
return nil, alerting.ValidationError{Reason: "Invalid Threema Gateway ID: Must be 8 characters long"}
}
if recipientID == "" {
return nil, alerting.ValidationError{Reason: "Could not find Threema Recipient ID in settings"}
}
if len(recipientID) != 8 {
return nil, alerting.ValidationError{Reason: "Invalid Threema Recipient ID: Must be 8 characters long"}
}
if apiSecret == "" {
return nil, alerting.ValidationError{Reason: "Could not find Threema API secret in settings"}
}
return &ThreemaNotifier{
NotifierBase: old_notifiers.NewNotifierBase(&models.AlertNotification{
Uid: model.UID,
Name: model.Name,
Type: model.Type,
DisableResolveMessage: model.DisableResolveMessage,
Settings: model.Settings,
}),
GatewayID: gatewayID,
RecipientID: recipientID,
APISecret: apiSecret,
log: log.New("alerting.notifier.threema"),
tmpl: t,
}, nil
}
// Notify send an alert notification to Threema
func (tn *ThreemaNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) {
tn.log.Debug("Sending threema alert notification", "from", tn.GatewayID, "to", tn.RecipientID)
tmplData := notify.GetTemplateData(ctx, tn.tmpl, as, gokit_log.NewNopLogger())
var tmplErr error
tmpl := notify.TmplText(tn.tmpl, tmplData, &tmplErr)
// Set up basic API request data
data := url.Values{}
data.Set("from", tn.GatewayID)
data.Set("to", tn.RecipientID)
data.Set("secret", tn.APISecret)
// Determine emoji
stateEmoji := "\u26A0\uFE0F " // Warning sign
alerts := types.Alerts(as...)
if alerts.Status() == model.AlertResolved {
stateEmoji = "\u2705 " // Check Mark Button
}
// Build message
message := fmt.Sprintf("%s%s\n\n*Message:*\n%s\n*URL:* %s\n",
stateEmoji,
tmpl(`{{ template "default.title" . }}`),
tmpl(`{{ template "default.message" . }}`),
path.Join(tn.tmpl.ExternalURL.String(), "/alerting/list"),
)
data.Set("text", message)
if tmplErr != nil {
return false, fmt.Errorf("failed to template Theema message: %w", tmplErr)
}
cmd := &models.SendWebhookSync{
Url: ThreemaGwBaseURL,
Body: data.Encode(),
HttpMethod: "POST",
HttpHeader: map[string]string{
"Content-Type": "application/x-www-form-urlencoded",
},
}
if err := bus.DispatchCtx(ctx, cmd); err != nil {
tn.log.Error("Failed to send threema notification", "error", err, "webhook", tn.Name)
return false, err
}
return true, nil
}
func (tn *ThreemaNotifier) SendResolved() bool {
return !tn.GetDisableResolveMessage()
}

View File

@ -0,0 +1,141 @@
package channels
import (
"context"
"net/url"
"testing"
"github.com/prometheus/alertmanager/notify"
"github.com/prometheus/alertmanager/types"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
)
func TestThreemaNotifier(t *testing.T) {
tmpl := templateForTests(t)
externalURL, err := url.Parse("http://localhost")
require.NoError(t, err)
tmpl.ExternalURL = externalURL
cases := []struct {
name string
settings string
alerts []*types.Alert
expMsg string
expInitError error
expMsgError error
}{
{
name: "One alert",
settings: `{
"gateway_id": "*1234567",
"recipient_id": "87654321",
"api_secret": "supersecret"
}`,
alerts: []*types.Alert{
{
Alert: model.Alert{
Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val1"},
Annotations: model.LabelSet{"ann1": "annv1"},
},
},
},
expMsg: "from=%2A1234567&secret=supersecret&text=%E2%9A%A0%EF%B8%8F+%5BFIRING%3A1%5D++%28val1%29%0A%0A%2AMessage%3A%2A%0A%0A%2A%2AFiring%2A%2A%0ALabels%3A%0A+-+alertname+%3D+alert1%0A+-+lbl1+%3D+val1%0AAnnotations%3A%0A+-+ann1+%3D+annv1%0ASource%3A+%0A%0A%0A%0A%0A%0A%2AURL%3A%2A+http%3A%2Flocalhost%2Falerting%2Flist%0A&to=87654321",
expInitError: nil,
expMsgError: nil,
}, {
name: "Multiple alerts",
settings: `{
"gateway_id": "*1234567",
"recipient_id": "87654321",
"api_secret": "supersecret"
}`,
alerts: []*types.Alert{
{
Alert: model.Alert{
Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val1"},
Annotations: model.LabelSet{"ann1": "annv1"},
},
}, {
Alert: model.Alert{
Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val2"},
Annotations: model.LabelSet{"ann1": "annv2"},
},
},
},
expMsg: "from=%2A1234567&secret=supersecret&text=%E2%9A%A0%EF%B8%8F+%5BFIRING%3A2%5D++%0A%0A%2AMessage%3A%2A%0A%0A%2A%2AFiring%2A%2A%0ALabels%3A%0A+-+alertname+%3D+alert1%0A+-+lbl1+%3D+val1%0AAnnotations%3A%0A+-+ann1+%3D+annv1%0ASource%3A+%0ALabels%3A%0A+-+alertname+%3D+alert1%0A+-+lbl1+%3D+val2%0AAnnotations%3A%0A+-+ann1+%3D+annv2%0ASource%3A+%0A%0A%0A%0A%0A%0A%2AURL%3A%2A+http%3A%2Flocalhost%2Falerting%2Flist%0A&to=87654321",
expInitError: nil,
expMsgError: nil,
}, {
name: "Invalid gateway id",
settings: `{
"gateway_id": "12345678",
"recipient_id": "87654321",
"api_secret": "supersecret"
}`,
expInitError: alerting.ValidationError{Reason: "Invalid Threema Gateway ID: Must start with a *"},
}, {
name: "Invalid receipent id",
settings: `{
"gateway_id": "*1234567",
"recipient_id": "8765432",
"api_secret": "supersecret"
}`,
expInitError: alerting.ValidationError{Reason: "Invalid Threema Recipient ID: Must be 8 characters long"},
}, {
name: "No API secret",
settings: `{
"gateway_id": "*1234567",
"recipient_id": "87654321"
}`,
expInitError: alerting.ValidationError{Reason: "Could not find Threema API secret in settings"},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
settingsJSON, err := simplejson.NewJson([]byte(c.settings))
require.NoError(t, err)
m := &NotificationChannelConfig{
Name: "threema_testing",
Type: "threema",
Settings: settingsJSON,
}
pn, err := NewThreemaNotifier(m, tmpl)
if c.expInitError != nil {
require.Error(t, err)
require.Equal(t, c.expInitError.Error(), err.Error())
return
}
require.NoError(t, err)
body := ""
bus.AddHandlerCtx("test", func(ctx context.Context, webhook *models.SendWebhookSync) error {
body = webhook.Body
return nil
})
ctx := notify.WithGroupKey(context.Background(), "alertname")
ctx = notify.WithGroupLabels(ctx, model.LabelSet{"alertname": ""})
ok, err := pn.Notify(ctx, c.alerts...)
if c.expMsgError != nil {
require.False(t, ok)
require.Error(t, err)
require.Equal(t, c.expMsgError.Error(), err.Error())
return
}
require.NoError(t, err)
require.True(t, ok)
require.Equal(t, c.expMsg, body)
})
}
}

View File

@ -1405,6 +1405,63 @@ var expAvailableChannelJsonOutput = `
"secure": true
}
]
},
{
"type": "threema",
"name": "Threema Gateway",
"heading": "Threema Gateway settings",
"description": "Sends notifications to Threema using the Threema Gateway",
"info": "Notifications can be configured for any Threema Gateway ID of type \"Basic\". End-to-End IDs are not currently supported.The Threema Gateway ID can be set up at https://gateway.threema.ch/.",
"options": [
{
"element": "input",
"inputType": "text",
"label": "Gateway ID",
"description": "Your 8 character Threema Gateway ID (starting with a *).",
"placeholder": "*3MAGWID",
"propertyName": "gateway_id",
"selectOptions": null,
"showWhen": {
"field": "",
"is": ""
},
"required": true,
"validationRule": "\\*[0-9A-Z]{7}",
"secure": false
},
{
"element": "input",
"inputType": "text",
"label": "Recipient ID",
"description": "The 8 character Threema ID that should receive the alerts.",
"placeholder": "YOUR3MID",
"propertyName": "recipient_id",
"selectOptions": null,
"showWhen": {
"field": "",
"is": ""
},
"required": true,
"validationRule": "[0-9A-Z]{8}",
"secure": false
},
{
"element": "input",
"inputType": "text",
"label": "API Secret",
"description": "Your Threema Gateway API secret.",
"placeholder": "",
"propertyName": "api_secret",
"selectOptions": null,
"showWhen": {
"field": "",
"is": ""
},
"required": true,
"validationRule": "",
"secure": true
}
]
}
]
`