Alerting: Allow customization of Google chat message (#43568)

* Allow customizable googlechat message via optional setting

* Add optional message field in googlechat contact point configurator

* Fix strange error message on send if template fails to fully evaluate

* Elevate template evaluation failure logs to Warn level

* Extract default.title template embed from all channels to shared constant
This commit is contained in:
Alexander Weaver 2022-01-05 09:47:08 -06:00 committed by GitHub
parent b826804ef7
commit fd583a0e3b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 197 additions and 43 deletions

View File

@ -740,6 +740,12 @@ func GetAvailableNotifiers() []*alerting.NotifierPlugin {
PropertyName: "url",
Required: true,
},
{
Label: "Message",
Element: alerting.ElementTypeTextArea,
Placeholder: `{{ template "default.message" . }}`,
PropertyName: "message",
},
},
},
{

View File

@ -9,6 +9,8 @@ import (
"github.com/stretchr/testify/require"
)
const DefaultMessageTitleEmbed = `{{ template "default.title" . }}`
var DefaultTemplateString = `
{{ define "__subject" }}[{{ .Status | toUpper }}{{ if eq .Status "firing" }}:{{ .Alerts.Firing | len }}{{ end }}] {{ .GroupLabels.SortedPairs.Values | join " " }} {{ if gt (len .CommonLabels) (len .GroupLabels) }}({{ with .CommonLabels.Remove .GroupLabels.Names }}{{ .Values | join " " }}{{ end }}){{ end }}{{ end }}

View File

@ -85,7 +85,7 @@ func TestDefaultTemplateString(t *testing.T) {
expected string
}{
{
templateString: `{{ template "default.title" .}}`,
templateString: DefaultMessageTitleEmbed,
expected: `[FIRING:2] (alert1)`,
},
{

View File

@ -74,7 +74,7 @@ func (dd *DingDingNotifier) Notify(ctx context.Context, as ...*types.Alert) (boo
tmpl, _ := TmplText(ctx, dd.tmpl, as, dd.log, &tmplErr)
message := tmpl(dd.Message)
title := tmpl(`{{ template "default.title" . }}`)
title := tmpl(DefaultMessageTitleEmbed)
var bodyMsg map[string]interface{}
if tmpl(dd.MsgType) == "actionCard" {
@ -102,7 +102,7 @@ func (dd *DingDingNotifier) Notify(ctx context.Context, as ...*types.Alert) (boo
u := tmpl(dd.URL)
if tmplErr != nil {
dd.log.Debug("failed to template DingDing message", "err", tmplErr.Error())
dd.log.Warn("failed to template DingDing message", "err", tmplErr.Error())
}
body, err := json.Marshal(bodyMsg)

View File

@ -86,7 +86,7 @@ func (d DiscordNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool,
}
embed := simplejson.New()
embed.Set("title", tmpl(`{{ template "default.title" . }}`))
embed.Set("title", tmpl(DefaultMessageTitleEmbed))
embed.Set("footer", footer)
embed.Set("type", "rich")
@ -100,7 +100,7 @@ func (d DiscordNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool,
u := tmpl(d.WebhookURL)
if tmplErr != nil {
d.log.Debug("failed to template Discord message", "err", tmplErr.Error())
d.log.Warn("failed to template Discord message", "err", tmplErr.Error())
}
body, err := json.Marshal(bodyJSON)

View File

@ -63,7 +63,7 @@ func (en *EmailNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool,
var tmplErr error
tmpl, data := TmplText(ctx, en.tmpl, as, en.log, &tmplErr)
title := tmpl(`{{ template "default.title" . }}`)
title := tmpl(DefaultMessageTitleEmbed)
alertPageURL := en.tmpl.ExternalURL.String()
ruleURL := en.tmpl.ExternalURL.String()
@ -100,7 +100,7 @@ func (en *EmailNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool,
}
if tmplErr != nil {
en.log.Debug("failed to template email message", "err", tmplErr.Error())
en.log.Warn("failed to template email message", "err", tmplErr.Error())
}
if err := bus.Dispatch(ctx, cmd); err != nil {

View File

@ -19,9 +19,10 @@ import (
// alert notifications to Google chat.
type GoogleChatNotifier struct {
*Base
URL string
log log.Logger
tmpl *template.Template
URL string
log log.Logger
tmpl *template.Template
content string
}
func NewGoogleChatNotifier(model *NotificationChannelConfig, t *template.Template) (*GoogleChatNotifier, error) {
@ -34,6 +35,8 @@ func NewGoogleChatNotifier(model *NotificationChannelConfig, t *template.Templat
return nil, receiverInitError{Cfg: *model, Reason: "could not find url property in settings"}
}
content := model.Settings.Get("message").MustString(`{{ template "default.message" . }}`)
return &GoogleChatNotifier{
Base: NewBase(&models.AlertNotification{
Uid: model.UID,
@ -42,9 +45,10 @@ func NewGoogleChatNotifier(model *NotificationChannelConfig, t *template.Templat
DisableResolveMessage: model.DisableResolveMessage,
Settings: model.Settings,
}),
URL: url,
log: log.New("alerting.notifier.googlechat"),
tmpl: t,
URL: url,
log: log.New("alerting.notifier.googlechat"),
tmpl: t,
content: content,
}, nil
}
@ -57,7 +61,7 @@ func (gcn *GoogleChatNotifier) Notify(ctx context.Context, as ...*types.Alert) (
widgets := []widget{}
if msg := tmpl(`{{ template "default.message" . }}`); msg != "" {
if msg := tmpl(gcn.content); msg != "" {
// Add a text paragraph widget for the message if there is a message.
// Google Chat API doesn't accept an empty text property.
widgets = append(widgets, textParagraphWidget{
@ -67,6 +71,11 @@ func (gcn *GoogleChatNotifier) Notify(ctx context.Context, as ...*types.Alert) (
})
}
if tmplErr != nil {
gcn.log.Warn("failed to template Google Chat message", "err", tmplErr.Error())
tmplErr = nil
}
ruleURL := joinUrlPath(gcn.tmpl.ExternalURL.String(), "/alerting/list", gcn.log)
// Add a button widget (link to Grafana).
widgets = append(widgets, buttonWidget{
@ -93,12 +102,12 @@ func (gcn *GoogleChatNotifier) Notify(ctx context.Context, as ...*types.Alert) (
// Nest the required structs.
res := &outerStruct{
PreviewText: tmpl(`{{ template "default.title" . }}`),
FallbackText: tmpl(`{{ template "default.title" . }}`),
PreviewText: tmpl(DefaultMessageTitleEmbed),
FallbackText: tmpl(DefaultMessageTitleEmbed),
Cards: []card{
{
Header: header{
Title: tmpl(`{{ template "default.title" . }}`),
Title: tmpl(DefaultMessageTitleEmbed),
},
Sections: []section{
{
@ -111,7 +120,7 @@ func (gcn *GoogleChatNotifier) Notify(ctx context.Context, as ...*types.Alert) (
u := tmpl(gcn.URL)
if tmplErr != nil {
gcn.log.Debug("failed to template GoogleChat message", "err", tmplErr.Error())
gcn.log.Warn("failed to template GoogleChat message", "err", tmplErr.Error())
}
body, err := json.Marshal(res)

View File

@ -152,6 +152,114 @@ func TestGoogleChatNotifier(t *testing.T) {
name: "Error in initing",
settings: `{}`,
expInitError: `failed to validate receiver "googlechat_testing" of type "googlechat": could not find url property in settings`,
}, {
name: "Customized message",
settings: `{"url": "http://localhost", "message": "I'm a custom template and you have {{ len .Alerts.Firing }} firing alert."}`,
alerts: []*types.Alert{
{
Alert: model.Alert{
Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val1"},
Annotations: model.LabelSet{"ann1": "annv1", "__dashboardUid__": "abcd", "__panelId__": "efgh"},
},
},
},
expMsg: &outerStruct{
PreviewText: "[FIRING:1] (val1)",
FallbackText: "[FIRING:1] (val1)",
Cards: []card{
{
Header: header{
Title: "[FIRING:1] (val1)",
},
Sections: []section{
{
Widgets: []widget{
textParagraphWidget{
Text: text{
Text: "I'm a custom template and you have 1 firing alert.",
},
},
buttonWidget{
Buttons: []button{
{
TextButton: textButton{
Text: "OPEN IN GRAFANA",
OnClick: onClick{
OpenLink: openLink{
URL: "http://localhost/alerting/list",
},
},
},
},
},
},
textParagraphWidget{
Text: text{
// RFC822 only has the minute, hence it works in most cases.
Text: "Grafana v" + setting.BuildVersion + " | " + constNow.Format(time.RFC822),
},
},
},
},
},
},
},
},
expMsgError: nil,
}, {
name: "Invalid template",
settings: `{"url": "http://localhost", "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", "__dashboardUid__": "abcd", "__panelId__": "efgh"},
},
},
},
expMsg: &outerStruct{
PreviewText: "[FIRING:1] (val1)",
FallbackText: "[FIRING:1] (val1)",
Cards: []card{
{
Header: header{
Title: "[FIRING:1] (val1)",
},
Sections: []section{
{
Widgets: []widget{
textParagraphWidget{
Text: text{
Text: "I'm a custom template ",
},
},
buttonWidget{
Buttons: []button{
{
TextButton: textButton{
Text: "OPEN IN GRAFANA",
OnClick: onClick{
OpenLink: openLink{
URL: "http://localhost/alerting/list",
},
},
},
},
},
},
textParagraphWidget{
Text: text{
// RFC822 only has the minute, hence it works in most cases.
Text: "Grafana v" + setting.BuildVersion + " | " + constNow.Format(time.RFC822),
},
},
},
},
},
},
},
},
expMsgError: nil,
},
}

View File

@ -72,7 +72,7 @@ func (kn *KafkaNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool,
bodyJSON := simplejson.New()
bodyJSON.Set("alert_state", state)
bodyJSON.Set("description", tmpl(`{{ template "default.title" . }}`))
bodyJSON.Set("description", tmpl(DefaultMessageTitleEmbed))
bodyJSON.Set("client", "Grafana")
bodyJSON.Set("details", tmpl(`{{ template "default.message" . }}`))
@ -99,7 +99,7 @@ func (kn *KafkaNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool,
topicURL := strings.TrimRight(kn.Endpoint, "/") + "/topics/" + tmpl(kn.Topic)
if tmplErr != nil {
kn.log.Debug("failed to template Kafka message", "err", tmplErr.Error())
kn.log.Warn("failed to template Kafka message", "err", tmplErr.Error())
}
cmd := &models.SendWebhookSync{

View File

@ -65,12 +65,12 @@ func (ln *LineNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool, e
body := fmt.Sprintf(
"%s\n%s\n\n%s",
tmpl(`{{ template "default.title" . }}`),
tmpl(DefaultMessageTitleEmbed),
ruleURL,
tmpl(`{{ template "default.message" . }}`),
)
if tmplErr != nil {
ln.log.Debug("failed to template Line message", "err", tmplErr.Error())
ln.log.Warn("failed to template Line message", "err", tmplErr.Error())
}
form := url.Values{}

View File

@ -156,10 +156,10 @@ func (on *OpsgenieNotifier) buildOpsgenieMessage(ctx context.Context, alerts mod
var tmplErr error
tmpl, data := TmplText(ctx, on.tmpl, as, on.log, &tmplErr)
title := tmpl(`{{ template "default.title" . }}`)
title := tmpl(DefaultMessageTitleEmbed)
description := fmt.Sprintf(
"%s\n%s\n\n%s",
tmpl(`{{ template "default.title" . }}`),
tmpl(DefaultMessageTitleEmbed),
ruleURL,
tmpl(`{{ template "default.message" . }}`),
)
@ -207,7 +207,7 @@ func (on *OpsgenieNotifier) buildOpsgenieMessage(ctx context.Context, alerts mod
apiURL = tmpl(on.APIUrl)
if tmplErr != nil {
on.log.Debug("failed to template Opsgenie message", "err", tmplErr.Error())
on.log.Warn("failed to template Opsgenie message", "err", tmplErr.Error())
}
return bodyJSON, apiURL, nil

View File

@ -72,7 +72,7 @@ func NewPagerdutyNotifier(model *NotificationChannelConfig, t *template.Template
Class: model.Settings.Get("class").MustString("default"),
Component: model.Settings.Get("component").MustString("Grafana"),
Group: model.Settings.Get("group").MustString("default"),
Summary: model.Settings.Get("summary").MustString(`{{ template "default.title" . }}`),
Summary: model.Settings.Get("summary").MustString(DefaultMessageTitleEmbed),
tmpl: t,
log: log.New("alerting.notifier." + model.Name),
}, nil
@ -145,7 +145,7 @@ func (pn *PagerdutyNotifier) buildPagerdutyMessage(ctx context.Context, alerts m
HRef: pn.tmpl.ExternalURL.String(),
Text: "External URL",
}},
Description: tmpl(`{{ template "default.title" . }}`), // TODO: this can be configurable template.
Description: tmpl(DefaultMessageTitleEmbed), // TODO: this can be configurable template.
Payload: pagerDutyPayload{
Component: tmpl(pn.Component),
Summary: tmpl(pn.Summary),
@ -167,7 +167,7 @@ func (pn *PagerdutyNotifier) buildPagerdutyMessage(ctx context.Context, alerts m
}
if tmplErr != nil {
pn.log.Debug("failed to template PagerDuty message", "err", tmplErr.Error())
pn.log.Warn("failed to template PagerDuty message", "err", tmplErr.Error())
}
return msg, eventType, nil

View File

@ -195,7 +195,7 @@ func (pn *PushoverNotifier) genPushoverBody(ctx context.Context, as ...*types.Al
}
// Add title
err = w.WriteField("title", tmpl(`{{ template "default.title" . }}`))
err = w.WriteField("title", tmpl(DefaultMessageTitleEmbed))
if err != nil {
return nil, b, err
}
@ -218,7 +218,7 @@ func (pn *PushoverNotifier) genPushoverBody(ctx context.Context, as ...*types.Al
}
if tmplErr != nil {
pn.log.Debug("failed to template pushover message", "err", tmplErr.Error())
pn.log.Warn("failed to template pushover message", "err", tmplErr.Error())
}
// Mark as html message

View File

@ -128,7 +128,7 @@ func (sn *SensuGoNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool
}
if tmplErr != nil {
sn.log.Debug("failed to template sensugo message", "err", tmplErr.Error())
sn.log.Warn("failed to template sensugo message", "err", tmplErr.Error())
}
body, err := json.Marshal(bodyMsgType)

View File

@ -118,7 +118,7 @@ func NewSlackNotifier(model *NotificationChannelConfig, t *template.Template, fn
IconURL: model.Settings.Get("icon_url").MustString(),
Token: token,
Text: model.Settings.Get("text").MustString(`{{ template "default.message" . }}`),
Title: model.Settings.Get("title").MustString(`{{ template "default.title" . }}`),
Title: model.Settings.Get("title").MustString(DefaultMessageTitleEmbed),
log: log.New("alerting.notifier.slack"),
tmpl: t,
}, nil
@ -269,7 +269,7 @@ func (sn *SlackNotifier) buildSlackMessage(ctx context.Context, as []*types.Aler
},
}
if tmplErr != nil {
sn.log.Debug("failed to template Slack message", "err", tmplErr.Error())
sn.log.Warn("failed to template Slack message", "err", tmplErr.Error())
}
mentionsBuilder := strings.Builder{}

View File

@ -56,7 +56,7 @@ func (tn *TeamsNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool,
ruleURL := joinUrlPath(tn.tmpl.ExternalURL.String(), "/alerting/list", tn.log)
title := tmpl(`{{ template "default.title" . }}`)
title := tmpl(DefaultMessageTitleEmbed)
body := map[string]interface{}{
"@type": "MessageCard",
"@context": "http://schema.org/extensions",
@ -88,7 +88,7 @@ func (tn *TeamsNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool,
u := tmpl(tn.URL)
if tmplErr != nil {
tn.log.Debug("failed to template Teams message", "err", tmplErr.Error())
tn.log.Warn("failed to template Teams message", "err", tmplErr.Error())
}
b, err := json.Marshal(&body)

View File

@ -127,7 +127,7 @@ func (tn *TelegramNotifier) buildTelegramMessage(ctx context.Context, as []*type
message := tmpl(tn.Message)
if tmplErr != nil {
tn.log.Debug("failed to template Telegram message", "err", tmplErr.Error())
tn.log.Warn("failed to template Telegram message", "err", tmplErr.Error())
}
msg["text"] = message

View File

@ -101,14 +101,14 @@ func (tn *ThreemaNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool
// Build message
message := fmt.Sprintf("%s%s\n\n*Message:*\n%s\n*URL:* %s\n",
stateEmoji,
tmpl(`{{ template "default.title" . }}`),
tmpl(DefaultMessageTitleEmbed),
tmpl(`{{ template "default.message" . }}`),
path.Join(tn.tmpl.ExternalURL.String(), "/alerting/list"),
)
data.Set("text", message)
if tmplErr != nil {
tn.log.Debug("failed to template Threema message", "err", tmplErr.Error())
tn.log.Warn("failed to template Threema message", "err", tmplErr.Error())
}
cmd := &models.SendWebhookSync{

View File

@ -87,7 +87,7 @@ func (vn *VictoropsNotifier) Notify(ctx context.Context, as ...*types.Alert) (bo
bodyJSON := simplejson.New()
bodyJSON.Set("message_type", messageType)
bodyJSON.Set("entity_id", groupKey.Hash())
bodyJSON.Set("entity_display_name", tmpl(`{{ template "default.title" . }}`))
bodyJSON.Set("entity_display_name", tmpl(DefaultMessageTitleEmbed))
bodyJSON.Set("timestamp", time.Now().Unix())
bodyJSON.Set("state_message", tmpl(`{{ template "default.message" . }}`))
bodyJSON.Set("monitoring_tool", "Grafana v"+setting.BuildVersion)
@ -97,7 +97,7 @@ func (vn *VictoropsNotifier) Notify(ctx context.Context, as ...*types.Alert) (bo
u := tmpl(vn.URL)
if tmplErr != nil {
vn.log.Debug("failed to template VictorOps message", "err", tmplErr.Error())
vn.log.Warn("failed to template VictorOps message", "err", tmplErr.Error())
}
b, err := bodyJSON.MarshalJSON()

View File

@ -92,7 +92,7 @@ func (wn *WebhookNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool
GroupKey: groupKey.String(),
TruncatedAlerts: numTruncated,
OrgID: wn.orgID,
Title: tmpl(`{{ template "default.title" . }}`),
Title: tmpl(DefaultMessageTitleEmbed),
Message: tmpl(`{{ template "default.message" . }}`),
}
if types.Alerts(as...).Status() == model.AlertFiring {
@ -102,7 +102,7 @@ func (wn *WebhookNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool
}
if tmplErr != nil {
wn.log.Debug("failed to template webhook message", "err", tmplErr.Error())
wn.log.Warn("failed to template webhook message", "err", tmplErr.Error())
}
body, err := json.Marshal(msg)

View File

@ -56,7 +56,7 @@ func (w *WeComNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool, e
"msgtype": "markdown",
}
content := fmt.Sprintf("# %s\n%s\n",
tmpl(`{{ template "default.title" . }}`),
tmpl(DefaultMessageTitleEmbed),
tmpl(w.Message),
)

View File

@ -1487,6 +1487,22 @@ var expAvailableChannelJsonOutput = `
"required": true,
"validationRule": "",
"secure": false
},
{
"element": "textarea",
"inputType": "",
"label": "Message",
"description": "",
"placeholder": "{{ template \"default.message\" . }}",
"propertyName": "message",
"selectOptions": null,
"showWhen": {
"field": "",
"is": ""
},
"required": false,
"validationRule": "",
"secure": false
}
]
},

View File

@ -81,6 +81,19 @@ export const grafanaNotifiersMock: NotifierDTO[] = [
validationRule: '',
secure: false,
},
{
element: 'textarea',
inputType: '',
label: 'Message',
description: '',
placeholder: '{{ template "default.message" . }}',
propertyName: 'message',
selectOptions: null,
showWhen: { field: '', is: '' },
required: false,
validationRule: '',
secure: false,
},
],
},
{