Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Alerting: Implement the Webex notifier #58480

Merged
merged 11 commits into from
Nov 11, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ require (
github.com/go-sql-driver/mysql v1.6.0
github.com/go-stack/stack v1.8.1
github.com/gobwas/glob v0.2.3
github.com/gofrs/uuid v4.3.0+incompatible // indirect
github.com/gofrs/uuid v4.3.0+incompatible
github.com/gogo/protobuf v1.3.2
github.com/golang/mock v1.6.0
github.com/golang/snappy v0.0.4
Expand Down
4 changes: 3 additions & 1 deletion pkg/services/ngalert/notifier/channels/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import (
"errors"
"strings"

"github.com/prometheus/alertmanager/template"

"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/notifications"
"github.com/prometheus/alertmanager/template"
)

type FactoryConfig struct {
Expand Down Expand Up @@ -65,6 +66,7 @@ var receiverFactories = map[string]func(FactoryConfig) (NotificationChannel, err
"victorops": VictorOpsFactory,
"webhook": WebHookFactory,
"wecom": WeComFactory,
"webex": WebexFactory,
}

func Factory(receiverType string) (func(FactoryConfig) (NotificationChannel, error), bool) {
Expand Down
175 changes: 175 additions & 0 deletions pkg/services/ngalert/notifier/channels/webex.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
package channels

import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"

"github.com/prometheus/alertmanager/template"
"github.com/prometheus/alertmanager/types"

"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/notifications"
)

const webexAPIEndpoint = "https://webexapis.com/v1/messages"

// WebexNotifier is responsible for sending alert notifications as webex messages.
type WebexNotifier struct {
*Base
ns notifications.WebhookSender
log log.Logger
images ImageStore
tmpl *template.Template
orgID int64
settings *webexSettings
}

// PLEASE do not touch these settings without taking a look at what we support as part of
// https://github.com/prometheus/alertmanager/blob/main/notify/webex/webex.go
gotjosh marked this conversation as resolved.
Show resolved Hide resolved
// Currently, the Alerting team is unifying channels and receivers - any discrepancy is detrimental to that.
type webexSettings struct {
Message string `json:"message,omitempty" yaml:"message,omitempty"`
RoomID string `json:"room_id,omitempty" yaml:"room_id,omitempty"`
WebhookURL string `json:"webhook_url,omitempty" yaml:"webhook_url,omitempty"`
Token string `json:"bot_token" yaml:"bot_token"`
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is my most recent discovery - looking at the Webex documentation it seems that the recommended approach is to use a "Bot", the "Bot" comes with a Token that you can use to interact with the Webex rooms.

As such, the required configuration appears to be: A bot token and which room you want to send alerts too (1 at a time).

I couldn't figure out a way to include the token/room as part of the URL as I previously thought, which means that we probably don't need to encrypt the URL and just the token?

}

func buildWebexSettings(factoryConfig FactoryConfig) (*webexSettings, error) {
settings := &webexSettings{}
err := factoryConfig.Config.unmarshalSettings(&settings)
if err != nil {
return settings, fmt.Errorf("failed to unmarshal settings: %w", err)
gotjosh marked this conversation as resolved.
Show resolved Hide resolved
}

if settings.WebhookURL == "" {
settings.WebhookURL = webexAPIEndpoint
}

if settings.Message == "" {
settings.Message = DefaultMessageEmbed
}

settings.Token = factoryConfig.DecryptFunc(context.Background(), factoryConfig.Config.SecureSettings, "bot_token", settings.Token)

webhookURL := factoryConfig.DecryptFunc(context.Background(), factoryConfig.Config.SecureSettings, "webhook_url", settings.WebhookURL)
u, err := url.Parse(webhookURL)
if err != nil {
return nil, fmt.Errorf("invalid URL %q", webhookURL)
}
settings.WebhookURL = u.String()

return settings, err
}

func WebexFactory(fc FactoryConfig) (NotificationChannel, error) {
notifier, err := buildWebexNotifier(fc)
if err != nil {
return nil, receiverInitError{
Reason: err.Error(),
Cfg: *fc.Config,
}
}
return notifier, nil
}

// buildWebexSettings is the constructor for the Webex notifier.
func buildWebexNotifier(factoryConfig FactoryConfig) (*WebexNotifier, error) {
settings, err := buildWebexSettings(factoryConfig)
if err != nil {
return nil, err
}

logger := log.New("alerting.notifier.webex")

return &WebexNotifier{
Base: NewBase(&models.AlertNotification{
Uid: factoryConfig.Config.UID,
Name: factoryConfig.Config.Name,
Type: factoryConfig.Config.Type,
DisableResolveMessage: factoryConfig.Config.DisableResolveMessage,
Settings: factoryConfig.Config.Settings,
}),
orgID: factoryConfig.Config.OrgID,
log: logger,
ns: factoryConfig.NotificationService,
images: factoryConfig.ImageStore,
tmpl: factoryConfig.Template,
settings: settings,
}, nil
}

// WebexMessage defines the JSON object to send to Webex endpoints.
type WebexMessage struct {
*ExtendedData

RoomID string `json:"roomId,omitempty"`
Message string `json:"markdown"`
gotjosh marked this conversation as resolved.
Show resolved Hide resolved
Files []string `json:"files"`
}

// Notify implements the Notifier interface.
func (wn *WebexNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) {
var tmplErr error
tmpl, data := TmplText(ctx, wn.tmpl, as, wn.log, &tmplErr)

message := tmpl(wn.settings.Message)

if tmplErr != nil {
wn.log.Warn("failed to template webex message", "error", tmplErr.Error())
tmplErr = nil
}

msg := &WebexMessage{
ExtendedData: data,
RoomID: wn.settings.RoomID,
Message: message,
}

// Augment our Alert data with ImageURLs if available.
_ = withStoredImages(ctx, wn.log, wn.images,
func(index int, image ngmodels.Image) error {
if len(image.URL) != 0 {
data.Alerts[index].ImageURL = image.URL
msg.Files[index] = image.URL
}
return nil
},
as...)

body, err := json.Marshal(msg)
if err != nil {
return false, err
}

parsedURL := tmpl(wn.settings.WebhookURL)
if tmplErr != nil {
return false, tmplErr
}

cmd := &models.SendWebhookSync{
Url: parsedURL,
Body: string(body),
HttpMethod: http.MethodPost,
}

if wn.settings.Token != "" {
headers := make(map[string]string)
headers["Authorization"] = fmt.Sprintf("Bearer %s", wn.settings.Token)
cmd.HttpHeader = headers
}

if err := wn.ns.SendWebhookSync(ctx, cmd); err != nil {
return false, err
}

return true, nil
}

func (wn *WebexNotifier) SendResolved() bool {
return !wn.GetDisableResolveMessage()
}
Original file line number Diff line number Diff line change
Expand Up @@ -1101,5 +1101,48 @@ func GetAvailableNotifiers() []*NotifierPlugin {
},
},
},
{
Type: "webex",
Name: "Cisco Webex",
Description: "Sends notifications to Cisco Webex",
Heading: "Webex settings",
Info: "Notifications can be configured for any Cisco Webex",
Options: []NotifierOption{
{
Label: "Cisco Webex Webhook URL",
Element: ElementTypeInput,
InputType: InputTypeText,
Placeholder: "https://api.ciscospark.com/v1/messages",
Description: "API endpoint at which we'll send webhooks to.",
PropertyName: "webhook_url",
},
{
Label: "Room ID",
Description: "The room ID to where we'll send messages to.",
Element: ElementTypeInput,
InputType: InputTypeText,
Placeholder: "GMtOWY0ZGJkNzMyMGFl",
PropertyName: "room_id",
Required: true,
},
{
Label: "Bot Token",
Description: "Non-expiring access token of the bot that will post messages on our behalf.",
Element: ElementTypeInput,
InputType: InputTypeText,
Placeholder: `xxxxxxxxxxxx`,
PropertyName: "bot_token",
Secure: true,
},
{
Label: "Message Template",
Description: "Message template to use. Markdown is supported.",
Element: ElementTypeInput,
InputType: InputTypeText,
Placeholder: `{{ template "default.message" . }}`,
PropertyName: "message",
},
},
},
}
}