Alerting: Opsgenie notification channel (#34418)

* Alerting: Opsgenie notification channel

This translate the opsgenie notification channel from the old alerting
system to the new alerting system with a few changes:

- The tag system has been replaced in favour of annotation.
- TBD
- TBD

Signed-off-by: Josue Abreu <josue@grafana.com>

* Fix template URL

* Bugfig: dont send resolved when autoClose is false

Signed-off-by: Ganesh Vernekar <ganeshvern@gmail.com>

* Fix integration tests

Signed-off-by: Ganesh Vernekar <ganeshvern@gmail.com>

* Fix URLs in all other channels

Signed-off-by: Ganesh Vernekar <ganeshvern@gmail.com>

Co-authored-by: Ganesh Vernekar <ganeshvern@gmail.com>
This commit is contained in:
gotjosh 2021-05-20 09:12:08 +01:00 committed by GitHub
parent 615de9bf34
commit 7b04278834
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 667 additions and 48 deletions

View File

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

View File

@ -1,6 +1,9 @@
package notifier
import "github.com/grafana/grafana/pkg/services/alerting"
import (
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/ngalert/notifier/channels"
)
// GetAvailableNotifiers returns the metadata of all the notification channels that can be configured.
func GetAvailableNotifiers() []*alerting.NotifierPlugin {
@ -745,5 +748,61 @@ func GetAvailableNotifiers() []*alerting.NotifierPlugin {
},
},
},
{
Type: "opsgenie",
Name: "OpsGenie",
Description: "Sends notifications to OpsGenie",
Heading: "OpsGenie settings",
Options: []alerting.NotifierOption{
{
Label: "API Key",
Element: alerting.ElementTypeInput,
InputType: alerting.InputTypeText,
Placeholder: "OpsGenie API Key",
PropertyName: "apiKey",
Required: true,
Secure: true,
},
{
Label: "Alert API Url",
Element: alerting.ElementTypeInput,
InputType: alerting.InputTypeText,
Placeholder: "https://api.opsgenie.com/v2/alerts",
PropertyName: "apiUrl",
Required: true,
},
{
Label: "Auto close incidents",
Element: alerting.ElementTypeCheckbox,
Description: "Automatically close alerts in OpsGenie once the alert goes back to ok.",
PropertyName: "autoClose",
}, {
Label: "Override priority",
Element: alerting.ElementTypeCheckbox,
Description: "Allow the alert priority to be set using the og_priority annotation",
PropertyName: "overridePriority",
},
{
Label: "Send notification tags as",
Element: alerting.ElementTypeSelect,
SelectOptions: []alerting.SelectOption{
{
Value: channels.OpsgenieSendTags,
Label: "Tags",
},
{
Value: channels.OpsgenieSendDetails,
Label: "Extra Properties",
},
{
Value: channels.OpsgenieSendBoth,
Label: "Tags & Extra Properties",
},
},
Description: "Send the common annotations to Opsgenie as either Extra Properties, Tags or both",
PropertyName: "sendTagsAs",
},
},
},
}
}

View File

@ -5,7 +5,6 @@ import (
"encoding/json"
"fmt"
"net/url"
"path"
gokit_log "github.com/go-kit/kit/log"
"github.com/prometheus/alertmanager/notify"
@ -65,9 +64,14 @@ type DingDingNotifier struct {
func (dd *DingDingNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) {
dd.log.Info("Sending dingding")
ruleURL, err := joinUrlPath(dd.tmpl.ExternalURL.String(), "/alerting/list")
if err != nil {
return false, err
}
q := url.Values{
"pc_slide": {"false"},
"url": {path.Join(dd.tmpl.ExternalURL.String(), "/alerting/list")},
"url": {ruleURL},
}
// Use special link to auto open the message url outside of Dingding

View File

@ -47,7 +47,7 @@ func TestDingdingNotifier(t *testing.T) {
expMsg: map[string]interface{}{
"msgtype": "link",
"link": map[string]interface{}{
"messageUrl": "dingtalk://dingtalkclient/page/link?pc_slide=false&url=http%3A%2Flocalhost%2Falerting%2Flist",
"messageUrl": "dingtalk://dingtalkclient/page/link?pc_slide=false&url=http%3A%2F%2Flocalhost%2Falerting%2Flist",
"text": "\n**Firing**\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSource: \n\n\n\n\n",
"title": "[FIRING:1] (val1)",
},
@ -77,7 +77,7 @@ func TestDingdingNotifier(t *testing.T) {
expMsg: map[string]interface{}{
"actionCard": map[string]interface{}{
"singleTitle": "More",
"singleURL": "dingtalk://dingtalkclient/page/link?pc_slide=false&url=http%3A%2Flocalhost%2Falerting%2Flist",
"singleURL": "dingtalk://dingtalkclient/page/link?pc_slide=false&url=http%3A%2F%2Flocalhost%2Falerting%2Flist",
"text": "2 alerts are firing, 0 are resolved",
"title": "[FIRING:2] ",
},

View File

@ -4,8 +4,6 @@ import (
"context"
"encoding/json"
"fmt"
"net/url"
"path"
"strconv"
"strings"
@ -85,12 +83,10 @@ func (d DiscordNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool,
color, _ := strconv.ParseInt(strings.TrimLeft(getAlertStatusColor(alerts.Status()), "#"), 16, 0)
embed.Set("color", color)
u, err := url.Parse(d.tmpl.ExternalURL.String())
ruleURL, err := joinUrlPath(d.tmpl.ExternalURL.String(), "/alerting/list")
if err != nil {
return false, fmt.Errorf("failed to parse external URL: %w", err)
return false, err
}
u.Path = path.Join(u.Path, "/alerting/list")
ruleURL := u.String()
embed.Set("url", ruleURL)
bodyJSON.Set("embeds", []interface{}{embed})

View File

@ -4,7 +4,6 @@ import (
"context"
"encoding/json"
"fmt"
"path"
"time"
gokit_log "github.com/go-kit/kit/log"
@ -69,6 +68,10 @@ func (gcn *GoogleChatNotifier) Notify(ctx context.Context, as ...*types.Alert) (
})
}
ruleURL, err := joinUrlPath(gcn.tmpl.ExternalURL.String(), "/alerting/list")
if err != nil {
return false, err
}
// Add a button widget (link to Grafana).
widgets = append(widgets, buttonWidget{
Buttons: []button{
@ -77,7 +80,7 @@ func (gcn *GoogleChatNotifier) Notify(ctx context.Context, as ...*types.Alert) (
Text: "OPEN IN GRAFANA",
OnClick: onClick{
OpenLink: openLink{
URL: path.Join(gcn.tmpl.ExternalURL.String(), "/alerting/list"),
URL: ruleURL,
},
},
},

View File

@ -68,7 +68,7 @@ func TestGoogleChatNotifier(t *testing.T) {
Text: "OPEN IN GRAFANA",
OnClick: onClick{
OpenLink: openLink{
URL: "http:/localhost/alerting/list",
URL: "http://localhost/alerting/list",
},
},
},
@ -128,7 +128,7 @@ func TestGoogleChatNotifier(t *testing.T) {
Text: "OPEN IN GRAFANA",
OnClick: onClick{
OpenLink: openLink{
URL: "http:/localhost/alerting/list",
URL: "http://localhost/alerting/list",
},
},
},

View File

@ -3,7 +3,6 @@ package channels
import (
"context"
"fmt"
"path"
gokit_log "github.com/go-kit/kit/log"
"github.com/prometheus/alertmanager/notify"
@ -76,7 +75,12 @@ func (kn *KafkaNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool,
bodyJSON.Set("description", tmpl(`{{ template "default.title" . }}`))
bodyJSON.Set("client", "Grafana")
bodyJSON.Set("details", tmpl(`{{ template "default.message" . }}`))
bodyJSON.Set("client_url", path.Join(kn.tmpl.ExternalURL.String(), "/alerting/list"))
ruleURL, err := joinUrlPath(kn.tmpl.ExternalURL.String(), "/alerting/list")
if err != nil {
return false, err
}
bodyJSON.Set("client_url", ruleURL)
groupKey, err := notify.ExtractGroupKey(ctx)
if err != nil {

View File

@ -52,7 +52,7 @@ func TestKafkaNotifier(t *testing.T) {
"value": {
"alert_state": "alerting",
"client": "Grafana",
"client_url": "http:/localhost/alerting/list",
"client_url": "http://localhost/alerting/list",
"description": "[FIRING:1] (val1)",
"details": "\n**Firing**\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSource: \n\n\n\n\n",
"incident_key": "6e3538104c14b583da237e9693b76debbc17f0f8058ef20492e5853096cf8733"
@ -88,7 +88,7 @@ func TestKafkaNotifier(t *testing.T) {
"value": {
"alert_state": "alerting",
"client": "Grafana",
"client_url": "http:/localhost/alerting/list",
"client_url": "http://localhost/alerting/list",
"description": "[FIRING:2] ",
"details": "\n**Firing**\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSource: \nLabels:\n - alertname = alert1\n - lbl1 = val2\nAnnotations:\n - ann1 = annv2\nSource: \n\n\n\n\n",
"incident_key": "6e3538104c14b583da237e9693b76debbc17f0f8058ef20492e5853096cf8733"

View File

@ -0,0 +1,227 @@
package channels
import (
"context"
"encoding/json"
"fmt"
"net/http"
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/components/simplejson"
"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"
"github.com/grafana/grafana/pkg/services/ngalert/logging"
)
const (
OpsgenieSendTags = "tags"
OpsgenieSendDetails = "details"
OpsgenieSendBoth = "both"
)
var (
OpsgenieAlertURL = "https://api.opsgenie.com/v2/alerts"
ValidPriorities = map[string]bool{"P1": true, "P2": true, "P3": true, "P4": true, "P5": true}
)
// OpsgenieNotifier is responsible for sending alert notifications to Opsgenie.
type OpsgenieNotifier struct {
old_notifiers.NotifierBase
APIKey string
APIUrl string
AutoClose bool
OverridePriority bool
SendTagsAs string
tmpl *template.Template
log log.Logger
}
// NewOpsgenieNotifier is the constructor for the Opsgenie notifier
func NewOpsgenieNotifier(model *NotificationChannelConfig, t *template.Template) (*OpsgenieNotifier, error) {
autoClose := model.Settings.Get("autoClose").MustBool(true)
overridePriority := model.Settings.Get("overridePriority").MustBool(true)
apiKey := model.DecryptedValue("apiKey", model.Settings.Get("apiKey").MustString())
apiURL := model.Settings.Get("apiUrl").MustString()
if apiKey == "" {
return nil, alerting.ValidationError{Reason: "Could not find api key property in settings"}
}
if apiURL == "" {
apiURL = OpsgenieAlertURL
}
sendTagsAs := model.Settings.Get("sendTagsAs").MustString(OpsgenieSendTags)
if sendTagsAs != OpsgenieSendTags && sendTagsAs != OpsgenieSendDetails && sendTagsAs != OpsgenieSendBoth {
return nil, alerting.ValidationError{
Reason: fmt.Sprintf("Invalid value for sendTagsAs: %q", sendTagsAs),
}
}
return &OpsgenieNotifier{
NotifierBase: old_notifiers.NewNotifierBase(&models.AlertNotification{
Uid: model.UID,
Name: model.Name,
Type: model.Type,
DisableResolveMessage: model.DisableResolveMessage,
Settings: model.Settings,
}),
APIKey: apiKey,
APIUrl: apiURL,
AutoClose: autoClose,
OverridePriority: overridePriority,
SendTagsAs: sendTagsAs,
tmpl: t,
log: log.New("alerting.notifier." + model.Name),
}, nil
}
// Notify sends an alert notification to Opsgenie
func (on *OpsgenieNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) {
on.log.Debug("Executing Opsgenie notification", "notification", on.Name)
alerts := types.Alerts(as...)
if alerts.Status() == model.AlertResolved && !on.SendResolved() {
on.log.Debug("Not sending a trigger to Opsgenie", "status", alerts.Status(), "auto resolve", on.SendResolved())
return true, nil
}
bodyJSON, url, err := on.buildOpsgenieMessage(ctx, alerts, as)
if err != nil {
return false, fmt.Errorf("build Opsgenie message: %w", err)
}
if url == "" {
// Resolved alert with no auto close.
// Hence skip sending anything.
return true, nil
}
body, err := json.Marshal(bodyJSON)
if err != nil {
return false, fmt.Errorf("marshal json: %w", err)
}
cmd := &models.SendWebhookSync{
Url: url,
Body: string(body),
HttpMethod: http.MethodPost,
HttpHeader: map[string]string{
"Content-Type": "application/json",
"Authorization": fmt.Sprintf("GenieKey %s", on.APIKey),
},
}
if err := bus.DispatchCtx(ctx, cmd); err != nil {
return false, fmt.Errorf("send notification to Opsgenie: %w", err)
}
return true, nil
}
func (on *OpsgenieNotifier) buildOpsgenieMessage(ctx context.Context, alerts model.Alerts, as []*types.Alert) (payload *simplejson.Json, apiURL string, err error) {
key, err := notify.ExtractGroupKey(ctx)
if err != nil {
return nil, "", err
}
var (
alias = key.Hash()
bodyJSON = simplejson.New()
details = simplejson.New()
)
if alerts.Status() == model.AlertResolved {
// For resolved notification, we only need the source.
// Don't need to run other templates.
if on.AutoClose {
bodyJSON := simplejson.New()
bodyJSON.Set("source", "Grafana")
apiURL = fmt.Sprintf("%s/%s/close?identifierType=alias", on.APIUrl, alias)
return bodyJSON, apiURL, nil
}
return nil, "", nil
}
ruleURL, err := joinUrlPath(on.tmpl.ExternalURL.String(), "/alerting/list")
if err != nil {
return nil, "", err
}
data := notify.GetTemplateData(ctx, on.tmpl, as, gokit_log.NewLogfmtLogger(logging.NewWrapper(on.log)))
var tmplErr error
tmpl := notify.TmplText(on.tmpl, data, &tmplErr)
title := tmpl(`{{ template "default.title" . }}`)
description := fmt.Sprintf(
"%s\n%s\n\n%s",
tmpl(`{{ template "default.title" . }}`),
ruleURL,
tmpl(`{{ template "default.message" . }}`),
)
var priority string
// In the new alerting system we've moved away from the grafana-tags. Instead, annotations on the rule itself should be used.
annotations := make(map[string]string, len(data.CommonAnnotations))
for k, v := range data.CommonAnnotations {
annotations[k] = tmpl(v)
if k == "og_priority" {
if ValidPriorities[v] {
priority = v
}
}
}
bodyJSON.Set("message", title)
bodyJSON.Set("source", "Grafana")
bodyJSON.Set("alias", alias)
bodyJSON.Set("description", description)
details.Set("url", ruleURL)
if on.sendDetails() {
for k, v := range annotations {
details.Set(k, v)
}
}
tags := make([]string, 0, len(annotations))
if on.sendTags() {
for k, v := range annotations {
tags = append(tags, fmt.Sprintf("%s:%s", k, v))
}
}
if priority != "" && on.OverridePriority {
bodyJSON.Set("priority", priority)
}
bodyJSON.Set("tags", tags)
bodyJSON.Set("details", details)
apiURL = on.APIUrl
if tmplErr != nil {
return nil, "", fmt.Errorf("failed to template Opsgenie message: %w", tmplErr)
}
return bodyJSON, apiURL, err
}
func (on *OpsgenieNotifier) SendResolved() bool {
return !on.GetDisableResolveMessage()
}
func (on *OpsgenieNotifier) sendDetails() bool {
return on.SendTagsAs == OpsgenieSendDetails || on.SendTagsAs == OpsgenieSendBoth
}
func (on *OpsgenieNotifier) sendTags() bool {
return on.SendTagsAs == OpsgenieSendTags || on.SendTagsAs == OpsgenieSendBoth
}

View File

@ -0,0 +1,206 @@
package channels
import (
"context"
"net/url"
"testing"
"time"
"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 TestOpsgenieNotifier(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: "Default config with one alert",
settings: `{"apiKey": "abcdefgh0123456789"}`,
alerts: []*types.Alert{
{
Alert: model.Alert{
Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val1"},
Annotations: model.LabelSet{"ann1": "annv1"},
},
},
},
expMsg: `{
"alias": "6e3538104c14b583da237e9693b76debbc17f0f8058ef20492e5853096cf8733",
"description": "[FIRING:1] (val1)\nhttp://localhost/alerting/list\n\n\n**Firing**\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSource: \n\n\n\n\n",
"details": {
"url": "http://localhost/alerting/list"
},
"message": "[FIRING:1] (val1)",
"source": "Grafana",
"tags": ["ann1:annv1"]
}`,
},
{
name: "Default config with one alert and send tags as tags",
settings: `{
"apiKey": "abcdefgh0123456789",
"sendTagsAs": "tags"
}`,
alerts: []*types.Alert{
{
Alert: model.Alert{
Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val1"},
Annotations: model.LabelSet{"ann1": "annv1"},
},
},
},
expMsg: `{
"alias": "6e3538104c14b583da237e9693b76debbc17f0f8058ef20492e5853096cf8733",
"description": "[FIRING:1] (val1)\nhttp://localhost/alerting/list\n\n\n**Firing**\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSource: \n\n\n\n\n",
"details": {
"url": "http://localhost/alerting/list"
},
"message": "[FIRING:1] (val1)",
"source": "Grafana",
"tags": ["ann1:annv1"]
}`,
},
{
name: "Default config with one alert and send tags as details",
settings: `{
"apiKey": "abcdefgh0123456789",
"sendTagsAs": "details"
}`,
alerts: []*types.Alert{
{
Alert: model.Alert{
Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val1"},
Annotations: model.LabelSet{"ann1": "annv1"},
},
},
},
expMsg: `{
"alias": "6e3538104c14b583da237e9693b76debbc17f0f8058ef20492e5853096cf8733",
"description": "[FIRING:1] (val1)\nhttp://localhost/alerting/list\n\n\n**Firing**\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSource: \n\n\n\n\n",
"details": {
"ann1": "annv1",
"url": "http://localhost/alerting/list"
},
"message": "[FIRING:1] (val1)",
"source": "Grafana",
"tags": []
}`,
},
{
name: "Custom config with multiple alerts and send tags as both details and tag",
settings: `{
"apiKey": "abcdefgh0123456789",
"sendTagsAs": "both"
}`,
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": "annv1"},
},
},
},
expMsg: `{
"alias": "6e3538104c14b583da237e9693b76debbc17f0f8058ef20492e5853096cf8733",
"description": "[FIRING:2] \nhttp://localhost/alerting/list\n\n\n**Firing**\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSource: \nLabels:\n - alertname = alert1\n - lbl1 = val2\nAnnotations:\n - ann1 = annv1\nSource: \n\n\n\n\n",
"details": {
"ann1": "annv1",
"url": "http://localhost/alerting/list"
},
"message": "[FIRING:2] ",
"source": "Grafana",
"tags": ["ann1:annv1"]
}`,
expInitError: nil,
expMsgError: nil,
},
{
name: "Resolved is not sent when auto close is false",
settings: `{"apiKey": "abcdefgh0123456789", "autoClose": false}`,
alerts: []*types.Alert{
{
Alert: model.Alert{
Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val1"},
Annotations: model.LabelSet{"ann1": "annv1"},
EndsAt: time.Now().Add(-1 * time.Minute),
},
},
},
},
{
name: "Error when incorrect settings",
settings: `{}`,
expInitError: alerting.ValidationError{Reason: "Could not find api key property 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: "opsgenie_testing",
Type: "opsgenie",
Settings: settingsJSON,
}
pn, err := NewOpsgenieNotifier(m, tmpl)
if c.expInitError != nil {
require.Error(t, err)
require.Equal(t, c.expInitError.Error(), err.Error())
return
}
require.NoError(t, err)
body := "<not-sent>"
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.True(t, ok)
require.NoError(t, err)
if c.expMsg == "" {
// No notification was expected.
require.Equal(t, "<not-sent>", body)
} else {
require.JSONEq(t, c.expMsg, body)
}
})
}
}

View File

@ -5,8 +5,6 @@ import (
"context"
"fmt"
"mime/multipart"
"net/url"
"path"
"strconv"
gokit_log "github.com/go-kit/kit/log"
@ -130,12 +128,10 @@ func (pn *PushoverNotifier) SendResolved() bool {
func (pn *PushoverNotifier) genPushoverBody(ctx context.Context, as ...*types.Alert) (map[string]string, bytes.Buffer, error) {
var b bytes.Buffer
u, err := url.Parse(pn.tmpl.ExternalURL.String())
ruleURL, err := joinUrlPath(pn.tmpl.ExternalURL.String(), "/alerting/list")
if err != nil {
return nil, b, fmt.Errorf("failed to parse ")
return nil, b, err
}
u.Path = path.Join(u.Path, "/alerting/list")
ruleURL := u.String()
alerts := types.Alerts(as...)

View File

@ -4,8 +4,6 @@ import (
"context"
"encoding/json"
"fmt"
"net/url"
"path"
"strings"
"time"
@ -109,12 +107,10 @@ func (sn *SensuGoNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool
handlers = []string{sn.Handler}
}
u, err := url.Parse(sn.tmpl.ExternalURL.String())
ruleURL, err := joinUrlPath(sn.tmpl.ExternalURL.String(), "/alerting/list")
if err != nil {
return false, fmt.Errorf("failed to parse external URL: %w", err)
return false, err
}
u.Path = path.Join(u.Path, "/alerting/list")
ruleURL := u.String()
bodyMsgType := map[string]interface{}{
"entity": map[string]interface{}{
"metadata": map[string]interface{}{

View File

@ -10,7 +10,6 @@ import (
"net"
"net/http"
"net/url"
"path"
"regexp"
"strings"
"time"
@ -251,6 +250,11 @@ func (sn *SlackNotifier) buildSlackMessage(ctx context.Context, as []*types.Aler
var tmplErr error
tmpl := notify.TmplText(sn.tmpl, data, &tmplErr)
ruleURL, err := joinUrlPath(sn.tmpl.ExternalURL.String(), "/alerting/list")
if err != nil {
return nil, err
}
req := &slackMessage{
Channel: tmpl(sn.Recipient),
Username: tmpl(sn.Username),
@ -264,7 +268,7 @@ func (sn *SlackNotifier) buildSlackMessage(ctx context.Context, as []*types.Aler
Footer: "Grafana v" + setting.BuildVersion,
FooterIcon: FooterIconURL,
Ts: time.Now().Unix(),
TitleLink: path.Join(sn.tmpl.ExternalURL.String(), "/alerting/list"),
TitleLink: ruleURL,
Text: tmpl(sn.Text),
Fields: nil, // TODO. Should be a config.
},

View File

@ -56,7 +56,7 @@ func TestSlackNotifier(t *testing.T) {
Attachments: []attachment{
{
Title: "[FIRING:1] (val1)",
TitleLink: "http:/localhost/alerting/list",
TitleLink: "http://localhost/alerting/list",
Text: "\n**Firing**\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSource: \n\n\n\n\n",
Fallback: "[FIRING:1] (val1)",
Fields: nil,
@ -92,7 +92,7 @@ func TestSlackNotifier(t *testing.T) {
Attachments: []attachment{
{
Title: "[FIRING:1] (val1)",
TitleLink: "http:/localhost/alerting/list",
TitleLink: "http://localhost/alerting/list",
Text: "\n**Firing**\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSource: \n\n\n\n\n",
Fallback: "[FIRING:1] (val1)",
Fields: nil,
@ -135,7 +135,7 @@ func TestSlackNotifier(t *testing.T) {
Attachments: []attachment{
{
Title: "2 firing, 0 resolved",
TitleLink: "http:/localhost/alerting/list",
TitleLink: "http://localhost/alerting/list",
Text: "\n**Firing**\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSource: \nLabels:\n - alertname = alert1\n - lbl1 = val2\nAnnotations:\n - ann1 = annv2\nSource: \n\n\n\n\n",
Fallback: "2 firing, 0 resolved",
Fields: nil,

View File

@ -3,7 +3,6 @@ package channels
import (
"context"
"encoding/json"
"path"
gokit_log "github.com/go-kit/kit/log"
"github.com/pkg/errors"
@ -61,6 +60,11 @@ func (tn *TeamsNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool,
var tmplErr error
tmpl := notify.TmplText(tn.tmpl, data, &tmplErr)
ruleURL, err := joinUrlPath(tn.tmpl.ExternalURL.String(), "/alerting/list")
if err != nil {
return false, err
}
title := tmpl(`{{ template "default.title" . }}`)
body := map[string]interface{}{
"@type": "MessageCard",
@ -84,7 +88,7 @@ func (tn *TeamsNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool,
"targets": []map[string]interface{}{
{
"os": "default",
"uri": path.Join(tn.tmpl.ExternalURL.String(), "/alerting/list"),
"uri": ruleURL,
},
},
},

View File

@ -61,7 +61,7 @@ func TestTeamsNotifier(t *testing.T) {
"@context": "http://schema.org",
"@type": "OpenUri",
"name": "View Rule",
"targets": []map[string]interface{}{{"os": "default", "uri": "http:/localhost/alerting/list"}},
"targets": []map[string]interface{}{{"os": "default", "uri": "http://localhost/alerting/list"}},
},
},
},
@ -103,7 +103,7 @@ func TestTeamsNotifier(t *testing.T) {
"@context": "http://schema.org",
"@type": "OpenUri",
"name": "View Rule",
"targets": []map[string]interface{}{{"os": "default", "uri": "http:/localhost/alerting/list"}},
"targets": []map[string]interface{}{{"os": "default", "uri": "http://localhost/alerting/list"}},
},
},
},

View File

@ -9,6 +9,7 @@ import (
"net"
"net/http"
"net/url"
"path"
"time"
"github.com/grafana/grafana/pkg/infra/log"
@ -110,3 +111,14 @@ var sendHTTPRequest = func(ctx context.Context, url *url.URL, cfg httpCfg, logge
logger.Debug("Sending HTTP request succeeded", "url", request.URL.String(), "statusCode", resp.Status)
return respBody, nil
}
func joinUrlPath(base, additionalPath string) (string, error) {
u, err := url.Parse(base)
if err != nil {
return "", fmt.Errorf("failed to parse URL: %w", err)
}
u.Path = path.Join(u.Path, additionalPath)
return u.String(), nil
}

View File

@ -2,7 +2,6 @@ package channels
import (
"context"
"path"
"strings"
"time"
@ -92,7 +91,12 @@ func (vn *VictoropsNotifier) Notify(ctx context.Context, as ...*types.Alert) (bo
bodyJSON.Set("timestamp", time.Now().Unix())
bodyJSON.Set("state_message", tmpl(`{{ template "default.message" . }}`))
bodyJSON.Set("monitoring_tool", "Grafana v"+setting.BuildVersion)
bodyJSON.Set("alert_url", path.Join(vn.tmpl.ExternalURL.String(), "/alerting/list"))
ruleURL, err := joinUrlPath(vn.tmpl.ExternalURL.String(), "/alerting/list")
if err != nil {
return false, err
}
bodyJSON.Set("alert_url", ruleURL)
b, err := bodyJSON.MarshalJSON()
if err != nil {

View File

@ -43,7 +43,7 @@ func TestVictoropsNotifier(t *testing.T) {
},
},
expMsg: `{
"alert_url": "http:/localhost/alerting/list",
"alert_url": "http://localhost/alerting/list",
"entity_display_name": "[FIRING:1] (val1)",
"entity_id": "6e3538104c14b583da237e9693b76debbc17f0f8058ef20492e5853096cf8733",
"message_type": "CRITICAL",
@ -69,7 +69,7 @@ func TestVictoropsNotifier(t *testing.T) {
},
},
expMsg: `{
"alert_url": "http:/localhost/alerting/list",
"alert_url": "http://localhost/alerting/list",
"entity_display_name": "[FIRING:2] ",
"entity_id": "6e3538104c14b583da237e9693b76debbc17f0f8058ef20492e5853096cf8733",
"message_type": "CRITICAL",

View File

@ -1462,6 +1462,108 @@ var expAvailableChannelJsonOutput = `
"secure": true
}
]
},
{
"type": "opsgenie",
"name": "OpsGenie",
"heading": "OpsGenie settings",
"description": "Sends notifications to OpsGenie",
"info": "",
"options": [
{
"element": "input",
"inputType": "text",
"label": "API Key",
"description": "",
"placeholder": "OpsGenie API Key",
"propertyName": "apiKey",
"selectOptions": null,
"showWhen": {
"field": "",
"is": ""
},
"required": true,
"validationRule": "",
"secure": true
},
{
"element": "input",
"inputType": "text",
"label": "Alert API Url",
"description": "",
"placeholder": "https://api.opsgenie.com/v2/alerts",
"propertyName": "apiUrl",
"selectOptions": null,
"showWhen": {
"field": "",
"is": ""
},
"required": true,
"validationRule": "",
"secure": false
},
{
"element": "checkbox",
"inputType": "",
"label": "Auto close incidents",
"description": "Automatically close alerts in OpsGenie once the alert goes back to ok.",
"placeholder": "",
"propertyName": "autoClose",
"selectOptions": null,
"showWhen": {
"field": "",
"is": ""
},
"required": false,
"validationRule": "",
"secure": false
},
{
"element": "checkbox",
"inputType": "",
"label": "Override priority",
"description": "Allow the alert priority to be set using the og_priority annotation",
"placeholder": "",
"propertyName": "overridePriority",
"selectOptions": null,
"showWhen": {
"field": "",
"is": ""
},
"required": false,
"validationRule": "",
"secure": false
},
{
"element": "select",
"inputType": "",
"label": "Send notification tags as",
"description": "Send the common annotations to Opsgenie as either Extra Properties, Tags or both",
"placeholder": "",
"propertyName": "sendTagsAs",
"selectOptions": [
{
"value": "tags",
"label": "Tags"
},
{
"value": "details",
"label": "Extra Properties"
},
{
"value": "both",
"label": "Tags & Extra Properties"
}
],
"showWhen": {
"field": "",
"is": ""
},
"required": false,
"validationRule": "",
"secure": false
}
]
}
]
`

View File

@ -749,7 +749,7 @@ var expNotifications = map[string][]string{
"attachments": [
{
"title": "Integration Test [FIRING:1] SlackAlert1 (UID_SlackAlert1)",
"title_link": "http:/localhost:3000/alerting/list",
"title_link": "http://localhost:3000/alerting/list",
"text": "Integration Test ",
"fallback": "Integration Test [FIRING:1] SlackAlert1 (UID_SlackAlert1)",
"footer": "Grafana v",
@ -776,7 +776,7 @@ var expNotifications = map[string][]string{
"attachments": [
{
"title": "[FIRING:1] SlackAlert2 (UID_SlackAlert2)",
"title_link": "http:/localhost:3000/alerting/list",
"title_link": "http://localhost:3000/alerting/list",
"text": "\n**Firing**\nLabels:\n - alertname = SlackAlert2\n - __alert_rule_uid__ = UID_SlackAlert2\nAnnotations:\nSource: \n\n\n\n\n",
"fallback": "[FIRING:1] SlackAlert2 (UID_SlackAlert2)",
"footer": "Grafana v",
@ -829,7 +829,7 @@ var expNotifications = map[string][]string{
"dingding_recv/dingding_test": {
`{
"link": {
"messageUrl": "dingtalk://dingtalkclient/page/link?pc_slide=false&url=http%3A%2Flocalhost%3A3000%2Falerting%2Flist",
"messageUrl": "dingtalk://dingtalkclient/page/link?pc_slide=false&url=http%3A%2F%2Flocalhost%3A3000%2Falerting%2Flist",
"text": "\n**Firing**\nLabels:\n - alertname = DingDingAlert\n - __alert_rule_uid__ = UID_DingDingAlert\nAnnotations:\nSource: \n\n\n\n\n",
"title": "[FIRING:1] DingDingAlert (UID_DingDingAlert)"
},
@ -848,7 +848,7 @@ var expNotifications = map[string][]string{
"targets": [
{
"os": "default",
"uri": "http:/localhost:3000/alerting/list"
"uri": "http://localhost:3000/alerting/list"
}
]
}