2021-05-19 11:24:04 -05:00
package channels
import (
"context"
"encoding/json"
2022-03-14 18:27:10 -05:00
"errors"
2021-05-19 11:24:04 -05:00
"fmt"
2022-05-26 11:23:39 -05:00
"net/url"
2021-05-19 11:24:04 -05:00
"time"
"github.com/prometheus/alertmanager/template"
"github.com/prometheus/alertmanager/types"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
2022-05-26 00:29:56 -05:00
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
2022-01-26 09:42:40 -06:00
"github.com/grafana/grafana/pkg/services/notifications"
2021-05-19 11:24:04 -05:00
"github.com/grafana/grafana/pkg/setting"
)
// GoogleChatNotifier is responsible for sending
// alert notifications to Google chat.
type GoogleChatNotifier struct {
2021-10-22 04:11:06 -05:00
* Base
2022-10-27 09:19:48 -05:00
log log . Logger
ns notifications . WebhookSender
images ImageStore
tmpl * template . Template
settings googleChatSettings
2021-05-19 11:24:04 -05:00
}
2022-10-27 09:19:48 -05:00
type googleChatSettings struct {
2022-03-14 18:27:10 -05:00
URL string
2022-10-27 09:19:48 -05:00
Title string
2022-03-14 18:27:10 -05:00
Content string
}
func GoogleChatFactory ( fc FactoryConfig ) ( NotificationChannel , error ) {
2022-10-27 09:19:48 -05:00
gcn , err := newGoogleChatNotifier ( fc )
2022-03-14 18:27:10 -05:00
if err != nil {
return nil , receiverInitError {
Reason : err . Error ( ) ,
Cfg : * fc . Config ,
}
2021-11-22 05:56:18 -06:00
}
2022-10-27 09:19:48 -05:00
return gcn , nil
2022-03-14 18:27:10 -05:00
}
2021-11-22 05:56:18 -06:00
2022-10-27 09:19:48 -05:00
func newGoogleChatNotifier ( fc FactoryConfig ) ( * GoogleChatNotifier , error ) {
var settings googleChatSettings
err := fc . Config . unmarshalSettings ( & settings )
if err != nil {
return nil , fmt . Errorf ( "failed to unmarshal settings: %w" , err )
}
URL := fc . Config . Settings . Get ( "url" ) . MustString ( )
if URL == "" {
2022-03-14 18:27:10 -05:00
return nil , errors . New ( "could not find url property in settings" )
2021-05-19 11:24:04 -05:00
}
return & GoogleChatNotifier {
2021-10-22 04:11:06 -05:00
Base : NewBase ( & models . AlertNotification {
2022-10-27 09:19:48 -05:00
Uid : fc . Config . UID ,
Name : fc . Config . Name ,
Type : fc . Config . Type ,
DisableResolveMessage : fc . Config . DisableResolveMessage ,
Settings : fc . Config . Settings ,
2021-05-19 11:24:04 -05:00
} ) ,
2022-10-27 09:19:48 -05:00
log : log . New ( "alerting.notifier.googlechat" ) ,
ns : fc . NotificationService ,
images : fc . ImageStore ,
tmpl : fc . Template ,
settings : googleChatSettings {
URL : URL ,
Title : fc . Config . Settings . Get ( "title" ) . MustString ( DefaultMessageTitleEmbed ) ,
Content : fc . Config . Settings . Get ( "message" ) . MustString ( DefaultMessageEmbed ) ,
} ,
} , nil
2021-05-19 11:24:04 -05:00
}
// Notify send an alert notification to Google Chat.
func ( gcn * GoogleChatNotifier ) Notify ( ctx context . Context , as ... * types . Alert ) ( bool , error ) {
2022-06-07 12:54:23 -05:00
gcn . log . Debug ( "executing Google Chat notification" )
2021-05-19 11:24:04 -05:00
var tmplErr error
2021-06-03 09:09:32 -05:00
tmpl , _ := TmplText ( ctx , gcn . tmpl , as , gcn . log , & tmplErr )
2021-05-19 11:24:04 -05:00
2022-10-27 09:19:48 -05:00
var widgets [ ] widget
2021-05-19 11:24:04 -05:00
2022-10-27 09:19:48 -05:00
if msg := tmpl ( gcn . settings . Content ) ; msg != "" {
2021-05-19 11:24:04 -05:00
// Add a text paragraph widget for the message if there is a message.
// Google Chat API doesn't accept an empty text property.
2022-10-27 09:19:48 -05:00
widgets = append ( widgets , textParagraphWidget { Text : text { Text : msg } } )
2021-05-19 11:24:04 -05:00
}
2022-01-05 09:47:08 -06:00
if tmplErr != nil {
2022-10-19 16:36:54 -05:00
gcn . log . Warn ( "failed to template Google Chat message" , "error" , tmplErr . Error ( ) )
2022-01-05 09:47:08 -06:00
tmplErr = nil
}
2021-06-03 09:09:32 -05:00
ruleURL := joinUrlPath ( gcn . tmpl . ExternalURL . String ( ) , "/alerting/list" , gcn . log )
2022-05-26 11:23:39 -05:00
if gcn . isUrlAbsolute ( ruleURL ) {
// Add a button widget (link to Grafana).
widgets = append ( widgets , buttonWidget {
Buttons : [ ] button {
{
TextButton : textButton {
Text : "OPEN IN GRAFANA" ,
OnClick : onClick {
OpenLink : openLink {
URL : ruleURL ,
} ,
2021-05-19 11:24:04 -05:00
} ,
} ,
} ,
} ,
2022-05-26 11:23:39 -05:00
} )
} else {
2022-06-07 12:54:23 -05:00
gcn . log . Warn ( "Grafana external URL setting is missing or invalid. Skipping 'open in grafana' button to prevent Google from displaying empty alerts." , "ruleURL" , ruleURL )
2022-05-26 11:23:39 -05:00
}
2021-05-19 11:24:04 -05:00
// Add text paragraph widget for the build version and timestamp.
widgets = append ( widgets , textParagraphWidget {
Text : text {
2021-10-19 15:18:44 -05:00
Text : "Grafana v" + setting . BuildVersion + " | " + ( timeNow ( ) ) . Format ( time . RFC822 ) ,
2021-05-19 11:24:04 -05:00
} ,
} )
2022-10-27 09:19:48 -05:00
title := tmpl ( gcn . settings . Title )
2021-05-19 11:24:04 -05:00
// Nest the required structs.
res := & outerStruct {
2022-10-27 09:19:48 -05:00
PreviewText : title ,
FallbackText : title ,
2021-05-19 11:24:04 -05:00
Cards : [ ] card {
{
2022-10-27 09:19:48 -05:00
Header : header { Title : title } ,
2021-05-19 11:24:04 -05:00
Sections : [ ] section {
2022-10-27 09:19:48 -05:00
{ Widgets : widgets } ,
2021-05-19 11:24:04 -05:00
} ,
} ,
} ,
}
2022-05-23 18:15:44 -05:00
if screenshots := gcn . buildScreenshotCard ( ctx , as ) ; screenshots != nil {
res . Cards = append ( res . Cards , * screenshots )
}
2021-05-19 11:24:04 -05:00
if tmplErr != nil {
2022-10-19 16:36:54 -05:00
gcn . log . Warn ( "failed to template GoogleChat message" , "error" , tmplErr . Error ( ) )
2022-03-31 13:57:48 -05:00
tmplErr = nil
}
2022-10-27 09:19:48 -05:00
u := tmpl ( gcn . settings . URL )
2022-03-31 13:57:48 -05:00
if tmplErr != nil {
2022-10-27 09:19:48 -05:00
gcn . log . Warn ( "failed to template GoogleChat URL" , "error" , tmplErr . Error ( ) , "fallback" , gcn . settings . URL )
u = gcn . settings . URL
2021-05-19 11:24:04 -05:00
}
body , err := json . Marshal ( res )
if err != nil {
return false , fmt . Errorf ( "marshal json: %w" , err )
}
cmd := & models . SendWebhookSync {
2021-06-22 04:42:54 -05:00
Url : u ,
2021-05-19 11:24:04 -05:00
HttpMethod : "POST" ,
HttpHeader : map [ string ] string {
"Content-Type" : "application/json; charset=UTF-8" ,
} ,
Body : string ( body ) ,
}
2022-01-26 09:42:40 -06:00
if err := gcn . ns . SendWebhookSync ( ctx , cmd ) ; err != nil {
2021-05-19 11:24:04 -05:00
gcn . log . Error ( "Failed to send Google Hangouts Chat alert" , "error" , err , "webhook" , gcn . Name )
return false , err
}
return true , nil
}
func ( gcn * GoogleChatNotifier ) SendResolved ( ) bool {
return ! gcn . GetDisableResolveMessage ( )
}
2022-05-26 11:23:39 -05:00
func ( gcn * GoogleChatNotifier ) isUrlAbsolute ( urlToCheck string ) bool {
parsed , err := url . Parse ( urlToCheck )
if err != nil {
2022-06-07 12:54:23 -05:00
gcn . log . Warn ( "could not parse URL" , "urlToCheck" , urlToCheck )
2022-05-26 11:23:39 -05:00
return false
}
return parsed . IsAbs ( )
}
2022-05-23 18:15:44 -05:00
func ( gcn * GoogleChatNotifier ) buildScreenshotCard ( ctx context . Context , alerts [ ] * types . Alert ) * card {
card := card {
2022-10-27 09:19:48 -05:00
Header : header { Title : "Screenshots" } ,
2022-05-23 18:15:44 -05:00
Sections : [ ] section { } ,
}
2022-05-26 00:29:56 -05:00
_ = withStoredImages ( ctx , gcn . log , gcn . images ,
2022-06-30 09:27:57 -05:00
func ( index int , image ngmodels . Image ) error {
if len ( image . URL ) == 0 {
2022-05-26 00:29:56 -05:00
return nil
2022-05-23 18:15:44 -05:00
}
section := section {
Widgets : [ ] widget {
textParagraphWidget {
Text : text {
2022-05-26 00:29:56 -05:00
Text : fmt . Sprintf ( "%s: %s" , alerts [ index ] . Status ( ) , alerts [ index ] . Name ( ) ) ,
2022-05-23 18:15:44 -05:00
} ,
} ,
2022-10-27 09:19:48 -05:00
imageWidget { Image : imageData { ImageURL : image . URL } } ,
2022-05-23 18:15:44 -05:00
} ,
}
card . Sections = append ( card . Sections , section )
2022-05-26 00:29:56 -05:00
return nil
} , alerts ... )
2022-05-23 18:15:44 -05:00
if len ( card . Sections ) == 0 {
return nil
}
return & card
}
2021-05-19 11:24:04 -05:00
// Structs used to build a custom Google Hangouts Chat message card.
// See: https://developers.google.com/hangouts/chat/reference/message-formats/cards
type outerStruct struct {
PreviewText string ` json:"previewText" `
FallbackText string ` json:"fallbackText" `
Cards [ ] card ` json:"cards" `
}
type card struct {
Header header ` json:"header" `
Sections [ ] section ` json:"sections" `
}
type header struct {
Title string ` json:"title" `
}
type section struct {
Widgets [ ] widget ` json:"widgets" `
}
// "generic" widget used to add different types of widgets (buttonWidget, textParagraphWidget, imageWidget)
type widget interface { }
type buttonWidget struct {
Buttons [ ] button ` json:"buttons" `
}
type textParagraphWidget struct {
Text text ` json:"textParagraph" `
}
2022-05-23 18:15:44 -05:00
type imageWidget struct {
Image imageData ` json:"image" `
}
type imageData struct {
ImageURL string ` json:"imageUrl" `
}
2021-05-19 11:24:04 -05:00
type text struct {
Text string ` json:"text" `
}
type button struct {
TextButton textButton ` json:"textButton" `
}
type textButton struct {
Text string ` json:"text" `
OnClick onClick ` json:"onClick" `
}
type onClick struct {
OpenLink openLink ` json:"openLink" `
}
type openLink struct {
URL string ` json:"url" `
}