Alerting: Use Adaptive Cards in Teams notifications (#53532)

This commit changes the cards in Teams notifications from Office 365
Connector cards to Adaptive Cards to fix an issue where images were not
shown in Teams for desktop and web. Since Office 365 Connector cards
are deprecated, it made sense to move to Adapative Cards and fix this
bug at the same time.

The Adaptive Card messages maintain the design of the Office 365
Connector Card with a number of minor differences:

- In Adaptive Card messages the color of the title is red or green
  depending on the status of the alerts, where as with Office 365
  connector cards there was a colored border at the top of the title

- In Adaptive Card messages the title is bold to make it easier to read
  when the color is red or green

- In Adaptive Card messages the thumbnails for images are medium size
  if there are more than two images, otherwise large size
This commit is contained in:
George Robinson 2022-08-10 19:51:20 +01:00 committed by GitHub
parent b198559225
commit 5e1d628f21
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 489 additions and 263 deletions

View File

@ -1,12 +1,15 @@
package channels
import (
"bytes"
"context"
"encoding/json"
"fmt"
"github.com/pkg/errors"
"github.com/prometheus/alertmanager/template"
"github.com/prometheus/alertmanager/types"
"github.com/prometheus/common/model"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
@ -14,18 +17,194 @@ import (
"github.com/grafana/grafana/pkg/services/notifications"
)
// TeamsNotifier is responsible for sending
// alert notifications to Microsoft teams.
type TeamsNotifier struct {
*Base
URL string
Message string
Title string
SectionTitle string
tmpl *template.Template
log log.Logger
ns notifications.WebhookSender
images ImageStore
const (
ImageSizeSmall = "small"
ImageSizeMedium = "medium"
ImageSizeLarge = "large"
TextColorDark = "dark"
TextColorLight = "light"
TextColorAccent = "accent"
TextColorGood = "good"
TextColorWarning = "warning"
TextColorAttention = "attention"
TextSizeSmall = "small"
TextSizeMedium = "medium"
TextSizeLarge = "large"
TextSizeExtraLarge = "extraLarge"
TextSizeDefault = "default"
TextWeightLighter = "lighter"
TextWeightBolder = "bolder"
TextWeightDefault = "default"
)
// AdaptiveCardsMessage represents a message for adaptive cards.
type AdaptiveCardsMessage struct {
Attachments []AdaptiveCardsAttachment `json:"attachments"`
Type string `json:"type"`
}
// NewAdaptiveCardsMessage returns a message prepared for adaptive cards.
// https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/connectors-using#send-adaptive-cards-using-an-incoming-webhook
func NewAdaptiveCardsMessage(card AdaptiveCard) AdaptiveCardsMessage {
return AdaptiveCardsMessage{
Attachments: []AdaptiveCardsAttachment{{
ContentType: "application/vnd.microsoft.card.adaptive",
Content: card,
}},
Type: "message",
}
}
// AdaptiveCardsAttachment contains an adaptive card.
type AdaptiveCardsAttachment struct {
Content AdaptiveCard `json:"content"`
ContentType string `json:"contentType"`
ContentURL string `json:"contentUrl,omitempty"`
}
// AdapativeCard repesents an Adaptive Card.
// https://adaptivecards.io/explorer/AdaptiveCard.html
type AdaptiveCard struct {
Body []AdaptiveCardItem `json:"body"`
Schema string `json:"$schema"`
Type string `json:"type"`
Version string `json:"version"`
}
// NewAdaptiveCard returns a prepared Adaptive Card.
func NewAdaptiveCard() AdaptiveCard {
return AdaptiveCard{
Body: make([]AdaptiveCardItem, 0),
Schema: "http://adaptivecards.io/schemas/adaptive-card.json",
Type: "AdaptiveCard",
Version: "1.4",
}
}
// AppendItem appends an item, such as text or an image, to the Adaptive Card.
func (c *AdaptiveCard) AppendItem(i AdaptiveCardItem) {
c.Body = append(c.Body, i)
}
// AdaptiveCardItem is an interface for adaptive card items such as containers, elements and inputs.
type AdaptiveCardItem interface {
MarshalJSON() ([]byte, error)
}
// AdaptiveCardTextBlockItem is a TextBlock.
type AdaptiveCardTextBlockItem struct {
Color string
Size string
Text string
Weight string
Wrap bool
}
func (i AdaptiveCardTextBlockItem) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
Text string `json:"text"`
Color string `json:"color,omitempty"`
Size string `json:"size,omitempty"`
Weight string `json:"weight,omitempty"`
Wrap bool `json:"wrap,omitempty"`
}{
Type: "TextBlock",
Text: i.Text,
Color: i.Color,
Size: i.Size,
Weight: i.Weight,
Wrap: i.Wrap,
})
}
// AdaptiveCardImageSetItem is an ImageSet.
type AdaptiveCardImageSetItem struct {
Images []AdaptiveCardImageItem
Size string
}
// AppendImage appends an image to image set.
func (i *AdaptiveCardImageSetItem) AppendImage(image AdaptiveCardImageItem) {
i.Images = append(i.Images, image)
}
func (i AdaptiveCardImageSetItem) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
Images []AdaptiveCardImageItem `json:"images"`
Size string `json:"imageSize"`
}{
Type: "ImageSet",
Images: i.Images,
Size: i.Size,
})
}
// AdaptiveCardImageItem is an Image.
type AdaptiveCardImageItem struct {
AltText string
Size string
URL string
}
func (i AdaptiveCardImageItem) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
URL string `json:"url"`
AltText string `json:"altText,omitempty"`
Size string `json:"size,omitempty"`
MsTeams map[string]interface{} `json:"msTeams,omitempty"`
}{
Type: "Image",
URL: i.URL,
AltText: i.AltText,
Size: i.Size,
MsTeams: map[string]interface{}{"allowExpand": true},
})
}
// AdaptiveCardActionSetItem is an ActionSet.
type AdaptiveCardActionSetItem struct {
Actions []AdaptiveCardActionItem
}
func (i AdaptiveCardActionSetItem) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
Actions []AdaptiveCardActionItem `json:"actions"`
}{
Type: "ActionSet",
Actions: i.Actions,
})
}
type AdaptiveCardActionItem interface {
MarshalJSON() ([]byte, error)
}
// AdapativeCardOpenURLActionItem is an Action.OpenUrl action.
type AdaptiveCardOpenURLActionItem struct {
IconURL string
Title string
URL string
}
func (i AdaptiveCardOpenURLActionItem) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
Title string `json:"title"`
URL string `json:"url"`
IconURL string `json:"iconUrl,omitempty"`
}{
Type: "Action.OpenUrl",
Title: i.Title,
URL: i.URL,
IconURL: i.IconURL,
})
}
type TeamsConfig struct {
@ -36,17 +215,6 @@ type TeamsConfig struct {
SectionTitle string
}
func TeamsFactory(fc FactoryConfig) (NotificationChannel, error) {
cfg, err := NewTeamsConfig(fc.Config)
if err != nil {
return nil, receiverInitError{
Reason: err.Error(),
Cfg: *fc.Config,
}
}
return NewTeamsNotifier(cfg, fc.NotificationService, fc.ImageStore, fc.Template), nil
}
func NewTeamsConfig(config *NotificationChannelConfig) (*TeamsConfig, error) {
URL := config.Settings.Get("url").MustString()
if URL == "" {
@ -61,8 +229,16 @@ func NewTeamsConfig(config *NotificationChannelConfig) (*TeamsConfig, error) {
}, nil
}
type teamsImage struct {
Image string `json:"image"`
type TeamsNotifier struct {
*Base
URL string
Message string
Title string
SectionTitle string
tmpl *template.Template
log log.Logger
ns notifications.WebhookSender
images ImageStore
}
// NewTeamsNotifier is the constructor for Teams notifier.
@ -86,60 +262,62 @@ func NewTeamsNotifier(config *TeamsConfig, ns notifications.WebhookSender, image
}
}
// Notify send an alert notification to Microsoft teams.
func TeamsFactory(fc FactoryConfig) (NotificationChannel, error) {
cfg, err := NewTeamsConfig(fc.Config)
if err != nil {
return nil, receiverInitError{
Reason: err.Error(),
Cfg: *fc.Config,
}
}
return NewTeamsNotifier(cfg, fc.NotificationService, fc.ImageStore, fc.Template), nil
}
func (tn *TeamsNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) {
var tmplErr error
tmpl, _ := TmplText(ctx, tn.tmpl, as, tn.log, &tmplErr)
ruleURL := joinUrlPath(tn.tmpl.ExternalURL.String(), "/alerting/list", tn.log)
card := NewAdaptiveCard()
card.AppendItem(AdaptiveCardTextBlockItem{
Color: getTeamsTextColor(types.Alerts(as...)),
Text: tmpl(tn.Title),
Size: TextSizeLarge,
Weight: TextWeightBolder,
Wrap: true,
})
card.AppendItem(AdaptiveCardTextBlockItem{
Text: tmpl(tn.Message),
Wrap: true,
})
var images []teamsImage
var s AdaptiveCardImageSetItem
_ = withStoredImages(ctx, tn.log, tn.images,
func(_ int, image ngmodels.Image) error {
if len(image.URL) != 0 {
images = append(images, teamsImage{Image: image.URL})
if image.URL != "" {
s.AppendImage(AdaptiveCardImageItem{URL: image.URL})
}
return nil
},
as...)
// Note: these template calls must remain in this order
title := tmpl(tn.Title)
sections := []map[string]interface{}{
{
"title": tmpl(tn.SectionTitle),
"text": tmpl(tn.Message),
},
if len(s.Images) > 2 {
s.Size = ImageSizeMedium
card.AppendItem(s)
} else if len(s.Images) > 0 {
s.Size = ImageSizeLarge
card.AppendItem(s)
}
if len(images) != 0 {
sections[0]["images"] = images
}
body := map[string]interface{}{
"@type": "MessageCard",
"@context": "http://schema.org/extensions",
// summary MUST not be empty or the webhook request fails
// summary SHOULD contain some meaningful information, since it is used for mobile notifications
"summary": title,
"title": title,
"themeColor": getAlertStatusColor(types.Alerts(as...).Status()),
"sections": sections,
"potentialAction": []map[string]interface{}{
{
"@context": "http://schema.org",
"@type": "OpenUri",
"name": "View Rule",
"targets": []map[string]interface{}{
{
"os": "default",
"uri": ruleURL,
},
},
card.AppendItem(AdaptiveCardActionSetItem{
Actions: []AdaptiveCardActionItem{
AdaptiveCardOpenURLActionItem{
Title: "View URL",
URL: joinUrlPath(tn.tmpl.ExternalURL.String(), "/alerting/list", tn.log),
},
},
}
})
// This check for tmplErr must happen before templating the URL
if tmplErr != nil {
tn.log.Warn("failed to template Teams message", "err", tmplErr.Error())
tmplErr = nil
@ -151,23 +329,20 @@ func (tn *TeamsNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool,
u = tn.URL
}
b, err := json.Marshal(&body)
b, err := json.Marshal(NewAdaptiveCardsMessage(card))
if err != nil {
return false, errors.Wrap(err, "marshal json")
return false, fmt.Errorf("failed to marshal JSON: %w", err)
}
cmd := &models.SendWebhookSync{Url: u, Body: string(b)}
// Teams does not always return non-2xx response when the request fails. Instead, the response body can contain an error message regardless of status code.
// Ex. 429 - Too Many Requests: https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/connectors-using?tabs=cURL#rate-limiting-for-connectors
// Teams sometimes does not use status codes to show when a request has failed. Instead, the
// response can contain an error message, irrespective of status code (i.e. https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/connectors-using?tabs=cURL#rate-limiting-for-connectors)
cmd.Validation = func(b []byte, statusCode int) error {
body := string(b)
// The request succeeded if the response is "1"
// https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/connectors-using?tabs=cURL#send-messages-using-curl-and-powershell
// Above states that if the POST succeeds, you must see a simple "1" output.
if body != "1" {
return errors.New(body)
if !bytes.Equal(b, []byte("1")) {
return errors.New(string(b))
}
return nil
}
@ -181,3 +356,11 @@ func (tn *TeamsNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool,
func (tn *TeamsNotifier) SendResolved() bool {
return !tn.GetDisableResolveMessage()
}
// getTeamsTextColor returns the text color for the message title.
func getTeamsTextColor(alerts model.Alerts) string {
if getAlertStatusColor(alerts.Status()) == ColorAlertFiring {
return TextColorAttention
}
return TextColorGood
}

View File

@ -34,193 +34,226 @@ func TestTeamsNotifier(t *testing.T) {
expMsg map[string]interface{}
expInitError string
expMsgError error
}{
{
name: "Default config with one alert",
settings: `{"url": "http://localhost"}`,
alerts: []*types.Alert{
{
Alert: model.Alert{
Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val1"},
Annotations: model.LabelSet{"ann1": "annv1", "__dashboardUid__": "abcd", "__panelId__": "efgh"},
},
}{{
name: "Default config with one alert",
settings: `{"url": "http://localhost"}`,
alerts: []*types.Alert{
{
Alert: model.Alert{
Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val1"},
Annotations: model.LabelSet{"ann1": "annv1", "__dashboardUid__": "abcd", "__panelId__": "efgh"},
},
},
expMsg: map[string]interface{}{
"@type": "MessageCard",
"@context": "http://schema.org/extensions",
"summary": "[FIRING:1] (val1)",
"title": "[FIRING:1] (val1)",
"themeColor": "#D63232",
"sections": []map[string]interface{}{
{
"title": "",
"text": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n",
},
},
expMsg: map[string]interface{}{
"attachments": []map[string]interface{}{{
"content": map[string]interface{}{
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"body": []map[string]interface{}{{
"color": "attention",
"size": "large",
"text": "[FIRING:1] (val1)",
"type": "TextBlock",
"weight": "bolder",
"wrap": true,
}, {
"text": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n",
"type": "TextBlock",
"wrap": true,
}, {
"actions": []map[string]interface{}{{
"title": "View URL",
"type": "Action.OpenUrl",
"url": "http://localhost/alerting/list",
}},
"type": "ActionSet",
}},
"type": "AdaptiveCard",
"version": "1.4",
},
"potentialAction": []map[string]interface{}{
{
"@context": "http://schema.org",
"@type": "OpenUri",
"name": "View Rule",
"targets": []map[string]interface{}{{"os": "default", "uri": "http://localhost/alerting/list"}},
},
"contentType": "application/vnd.microsoft.card.adaptive",
}},
"type": "message",
},
expMsgError: nil,
}, {
name: "Custom config with multiple alerts",
settings: `{
"url": "http://localhost",
"title": "{{ .CommonLabels.alertname }}",
"sectiontitle": "Details",
"message": "{{ len .Alerts.Firing }} alerts are firing, {{ len .Alerts.Resolved }} are resolved"
}`,
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"},
},
},
expMsgError: nil,
}, {
name: "Custom config with multiple alerts",
settings: `{
"url": "http://localhost",
"title": "{{ .CommonLabels.alertname }}",
"sectiontitle": "Details",
"message": "{{ len .Alerts.Firing }} alerts are firing, {{ len .Alerts.Resolved }} are resolved"
}`,
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: map[string]interface{}{
"attachments": []map[string]interface{}{{
"content": map[string]interface{}{
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"body": []map[string]interface{}{{
"color": "attention",
"size": "large",
"text": "alert1",
"type": "TextBlock",
"weight": "bolder",
"wrap": true,
}, {
"text": "2 alerts are firing, 0 are resolved",
"type": "TextBlock",
"wrap": true,
}, {
"actions": []map[string]interface{}{{
"title": "View URL",
"type": "Action.OpenUrl",
"url": "http://localhost/alerting/list",
}},
"type": "ActionSet",
}},
"type": "AdaptiveCard",
"version": "1.4",
},
"contentType": "application/vnd.microsoft.card.adaptive",
}},
"type": "message",
},
expMsgError: nil,
}, {
name: "Missing field in template",
settings: `{
"url": "http://localhost",
"title": "{{ .CommonLabels.alertname }}",
"sectiontitle": "Details",
"message": "I'm a custom template {{ .NotAField }} bad template"
}`,
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: map[string]interface{}{
"@type": "MessageCard",
"@context": "http://schema.org/extensions",
"summary": "alert1",
"title": "alert1",
"themeColor": "#D63232",
"sections": []map[string]interface{}{
{
"title": "Details",
"text": "2 alerts are firing, 0 are resolved",
},
},
expMsg: map[string]interface{}{
"attachments": []map[string]interface{}{{
"content": map[string]interface{}{
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"body": []map[string]interface{}{{
"color": "attention",
"size": "large",
"text": "alert1",
"type": "TextBlock",
"weight": "bolder",
"wrap": true,
}, {
"text": "I'm a custom template ",
"type": "TextBlock",
"wrap": true,
}, {
"actions": []map[string]interface{}{{
"title": "View URL",
"type": "Action.OpenUrl",
"url": "http://localhost/alerting/list",
}},
"type": "ActionSet",
}},
"type": "AdaptiveCard",
"version": "1.4",
},
"potentialAction": []map[string]interface{}{
{
"@context": "http://schema.org",
"@type": "OpenUri",
"name": "View Rule",
"targets": []map[string]interface{}{{"os": "default", "uri": "http://localhost/alerting/list"}},
},
},
},
expMsgError: nil,
}, {
name: "Missing field in template",
settings: `{
"url": "http://localhost",
"title": "{{ .CommonLabels.alertname }}",
"sectiontitle": "Details",
"message": "I'm a custom template {{ .NotAField }} bad template"
}`,
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: map[string]interface{}{
"@type": "MessageCard",
"@context": "http://schema.org/extensions",
"summary": "alert1",
"title": "alert1",
"themeColor": "#D63232",
"sections": []map[string]interface{}{
{
"title": "Details",
"text": "I'm a custom template ",
},
},
"potentialAction": []map[string]interface{}{
{
"@context": "http://schema.org",
"@type": "OpenUri",
"name": "View Rule",
"targets": []map[string]interface{}{{"os": "default", "uri": "http://localhost/alerting/list"}},
},
},
},
expMsgError: nil,
}, {
name: "Invalid template",
settings: `{
"contentType": "application/vnd.microsoft.card.adaptive",
}},
"type": "message",
},
expMsgError: nil,
}, {
name: "Invalid template",
settings: `{
"url": "http://localhost",
"title": "{{ .CommonLabels.alertname }}",
"sectiontitle": "Details",
"message": "I'm a custom template {{ {.NotAField }} bad template"
}`,
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"},
},
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: map[string]interface{}{
"@type": "MessageCard",
"@context": "http://schema.org/extensions",
"summary": "alert1",
"title": "alert1",
"themeColor": "#D63232",
"sections": []map[string]interface{}{
{
"title": "Details",
"text": "",
},
},
"potentialAction": []map[string]interface{}{
{
"@context": "http://schema.org",
"@type": "OpenUri",
"name": "View Rule",
"targets": []map[string]interface{}{{"os": "default", "uri": "http://localhost/alerting/list"}},
},
},
},
expMsgError: nil,
}, {
name: "Error in initing",
settings: `{}`,
expInitError: `could not find url property in settings`,
},
{
name: "webhook returns error message in body with 200",
settings: `{"url": "http://localhost"}`,
alerts: []*types.Alert{
{
Alert: model.Alert{
Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val1"},
Annotations: model.LabelSet{"ann1": "annv1", "__dashboardUid__": "abcd", "__panelId__": "efgh"},
},
expMsg: map[string]interface{}{
"attachments": []map[string]interface{}{{
"content": map[string]interface{}{
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"body": []map[string]interface{}{{
"color": "attention",
"size": "large",
"text": "alert1",
"type": "TextBlock",
"weight": "bolder",
"wrap": true,
}, {
"text": "",
"type": "TextBlock",
"wrap": true,
}, {
"actions": []map[string]interface{}{{
"title": "View URL",
"type": "Action.OpenUrl",
"url": "http://localhost/alerting/list",
}},
"type": "ActionSet",
}},
"type": "AdaptiveCard",
"version": "1.4",
},
"contentType": "application/vnd.microsoft.card.adaptive",
}},
"type": "message",
},
expMsgError: nil,
}, {
name: "Error in initing",
settings: `{}`,
expInitError: `could not find url property in settings`,
}, {
name: "webhook returns error message in body with 200",
settings: `{"url": "http://localhost"}`,
alerts: []*types.Alert{
{
Alert: model.Alert{
Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val1"},
Annotations: model.LabelSet{"ann1": "annv1", "__dashboardUid__": "abcd", "__panelId__": "efgh"},
},
},
response: &mockResponse{
status: 200,
body: "some error message",
error: nil,
},
expMsgError: errors.New("send notification to Teams: webhook failed validation: some error message"),
},
}
response: &mockResponse{
status: 200,
body: "some error message",
error: nil,
},
expMsgError: errors.New("send notification to Teams: webhook failed validation: some error message"),
}}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {

View File

@ -2196,30 +2196,40 @@ var expNonEmailNotifications = map[string][]string{
},
"teams_recv/teams_test": {
`{
"@context": "http://schema.org/extensions",
"@type": "MessageCard",
"potentialAction": [
"attachments": [
{
"@context": "http://schema.org",
"@type": "OpenUri",
"name": "View Rule",
"targets": [
{
"os": "default",
"uri": "http://localhost:3000/alerting/list"
}
]
}
"content": {
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"body": [
{
"color": "attention",
"size": "large",
"text": "[FIRING:1] TeamsAlert (default)",
"type": "TextBlock",
"weight": "bolder",
"wrap": true
}, {
"text": "**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = TeamsAlert\n - grafana_folder = default\nAnnotations:\nSource: http://localhost:3000/alerting/grafana/UID_TeamsAlert/view\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana\u0026matcher=alertname%3DTeamsAlert\u0026matcher=grafana_folder%3Ddefault\n",
"type": "TextBlock",
"wrap": true
}, {
"actions": [
{
"title": "View URL",
"type": "Action.OpenUrl",
"url": "http://localhost:3000/alerting/list"
}
],
"type": "ActionSet"
}
],
"type": "AdaptiveCard",
"version": "1.4"
},
"contentType": "application/vnd.microsoft.card.adaptive"
}
],
"sections": [
{
"text": "**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = TeamsAlert\n - grafana_folder = default\nAnnotations:\nSource: http://localhost:3000/alerting/grafana/UID_TeamsAlert/view\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DTeamsAlert&matcher=grafana_folder%3Ddefault\n",
"title": ""
}
],
"summary": "[FIRING:1] TeamsAlert (default)",
"themeColor": "#D63232",
"title": "[FIRING:1] TeamsAlert (default)"
"type": "message"
}`,
},
"webhook_recv/webhook_test": {