Skip to content

Commit

Permalink
Alerting: Implement the Webex notifier (#58480)
Browse files Browse the repository at this point in the history
* Alerting: Implement the Webex notifier

Closes #11750

Signed-off-by: gotjosh <josue.abreu@gmail.com>
  • Loading branch information
gotjosh committed Nov 11, 2022
1 parent 1c50390 commit d748979
Show file tree
Hide file tree
Showing 8 changed files with 418 additions and 2 deletions.
9 changes: 9 additions & 0 deletions docs/sources/administration/provisioning/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -632,6 +632,15 @@ The following sections detail the supported settings and secure settings for eac
| ---- |
| url |

#### Alert notification `Cisco Webex Teams`

| Name | Secure setting |
| --------- | -------------- |
| message | |
| room_id | |
| api_url | |
| bot_token | yes |

## Grafana Enterprise

Grafana Enterprise supports provisioning for the following resources:
Expand Down
1 change: 1 addition & 0 deletions docs/sources/alerting/fundamentals/contact-points/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ The following table lists the contact point types supported by Grafana.
| [Threema](https://threema.ch/) | `threema` | Supported | N/A |
| [VictorOps](https://help.victorops.com/) | `victorops` | Supported | Supported |
| [Webhook](#webhook) | `webhook` | Supported | Supported ([different format](https://prometheus.io/docs/alerting/latest/configuration/#webhook_config)) |
| [Cisco Webex Teams](#webex) | `webex` | Supported | Supported |
| [WeCom](#wecom) | `wecom` | Supported | N/A |
| [Zenduty](https://www.zenduty.com/) | `webhook` | Supported | N/A |

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ Images in notifications are supported in the following notifiers and additional
| Threema | No | No |
| VictorOps | No | No |
| Webhook | No | Yes |
| Cisco Webex Teams | No | Yes |

Include images from URL refers to using the external image store.

Expand Down
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
211 changes: 211 additions & 0 deletions pkg/services/ngalert/notifier/channels/webex.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
package channels

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

"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 webexAPIURL = "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
// Currently, the Alerting team is unifying channels and (upstream) 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"`
APIURL string `json:"api_url,omitempty" yaml:"api_url,omitempty"`
Token string `json:"bot_token" yaml:"bot_token"`
}

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

if settings.APIURL == "" {
settings.APIURL = webexAPIURL
}

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

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

u, err := url.Parse(settings.APIURL)
if err != nil {
return nil, fmt.Errorf("invalid URL %q", settings.APIURL)
}
settings.APIURL = 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 {
RoomID string `json:"roomId,omitempty"`
Message string `json:"markdown"`
Files []string `json:"files,omitempty"`
}

// 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, truncated := TruncateInBytes(tmpl(wn.settings.Message), 4096)
if truncated {
wn.log.Warn("Webex message too long, truncating message", "OriginalMessage", wn.settings.Message)
}

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

msg := &WebexMessage{
RoomID: wn.settings.RoomID,
Message: message,
Files: []string{},
}

// Augment our Alert data with ImageURLs if available.
_ = withStoredImages(ctx, wn.log, wn.images, func(index int, image ngmodels.Image) error {
// Cisco Webex only supports a single image per request: https://developer.webex.com/docs/basics#message-attachments
if image.HasURL() {
data.Alerts[index].ImageURL = image.URL
msg.Files = append(msg.Files, image.URL)
return ErrImagesDone
}

return nil
}, as...)

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

parsedURL := tmpl(wn.settings.APIURL)
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()
}

// Copied from https://github.com/prometheus/alertmanager/blob/main/notify/util.go, please remove once we're on-par with upstream.
// truncationMarker is the character used to represent a truncation.
const truncationMarker = "…"

// TruncateInBytes truncates a string to fit the given size in Bytes.
func TruncateInBytes(s string, n int) (string, bool) {
// First, measure the string the w/o a to-rune conversion.
if len(s) <= n {
return s, false
}

// The truncationMarker itself is 3 bytes, we can't return any part of the string when it's less than 3.
if n <= 3 {
switch n {
case 3:
return truncationMarker, true
default:
return strings.Repeat(".", n), true
}
}

// Now, to ensure we don't butcher the string we need to remove using runes.
r := []rune(s)
truncationTarget := n - 3

// Next, let's truncate the runes to the lower possible number.
truncatedRunes := r[:truncationTarget]
for len(string(truncatedRunes)) > truncationTarget {
truncatedRunes = r[:len(truncatedRunes)-1]
}

return string(truncatedRunes) + truncationMarker, true
}

0 comments on commit d748979

Please sign in to comment.