The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/pkg/services/ngalert/notifier/channels/victorops.go

171 lines
5.0 KiB

package channels
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"github.com/prometheus/alertmanager/notify"
"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"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/notifications"
"github.com/grafana/grafana/pkg/setting"
)
const (
// victoropsAlertStateCritical - Victorops uses "CRITICAL" string to indicate "Alerting" state
victoropsAlertStateCritical = "CRITICAL"
// victoropsAlertStateRecovery - VictorOps "RECOVERY" message type
victoropsAlertStateRecovery = "RECOVERY"
)
type victorOpsSettings struct {
URL string `json:"url,omitempty" yaml:"url,omitempty"`
MessageType string `json:"messageType,omitempty" yaml:"messageType,omitempty"`
}
func buildVictorOpsSettings(fc FactoryConfig) (victorOpsSettings, error) {
settings := victorOpsSettings{}
err := fc.Config.unmarshalSettings(&settings)
if err != nil {
return settings, fmt.Errorf("failed to unmarshal settings: %w", err)
}
if settings.URL == "" {
return settings, errors.New("could not find victorops url property in settings")
}
if settings.MessageType == "" {
settings.MessageType = victoropsAlertStateCritical
}
return settings, nil
}
func VictorOpsFactory(fc FactoryConfig) (NotificationChannel, error) {
notifier, err := NewVictoropsNotifier(fc)
if err != nil {
return nil, receiverInitError{
Reason: err.Error(),
Cfg: *fc.Config,
}
}
return notifier, nil
}
// NewVictoropsNotifier creates an instance of VictoropsNotifier that
// handles posting notifications to Victorops REST API
func NewVictoropsNotifier(fc FactoryConfig) (*VictoropsNotifier, error) {
settings, err := buildVictorOpsSettings(fc)
if err != nil {
return nil, err
}
return &VictoropsNotifier{
Base: NewBase(&models.AlertNotification{
Uid: fc.Config.UID,
Name: fc.Config.Name,
Type: fc.Config.Type,
DisableResolveMessage: fc.Config.DisableResolveMessage,
Settings: fc.Config.Settings,
}),
log: log.New("alerting.notifier.victorops"),
images: fc.ImageStore,
ns: fc.NotificationService,
tmpl: fc.Template,
settings: settings,
}, nil
}
// VictoropsNotifier defines URL property for Victorops REST API
// and handles notification process by formatting POST body according to
// Victorops specifications (http://victorops.force.com/knowledgebase/articles/Integration/Alert-Ingestion-API-Documentation/)
type VictoropsNotifier struct {
*Base
log log.Logger
images ImageStore
ns notifications.WebhookSender
tmpl *template.Template
settings victorOpsSettings
}
// Notify sends notification to Victorops via POST to URL endpoint
func (vn *VictoropsNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) {
vn.log.Debug("sending notification", "notification", vn.Name)
var tmplErr error
tmpl, _ := TmplText(ctx, vn.tmpl, as, vn.log, &tmplErr)
messageType := strings.ToUpper(tmpl(vn.settings.MessageType))
if messageType == "" {
vn.log.Warn("expansion of message type template resulted in an empty string. Using fallback", "fallback", victoropsAlertStateCritical, "template", vn.settings.MessageType)
messageType = victoropsAlertStateCritical
}
alerts := types.Alerts(as...)
if alerts.Status() == model.AlertResolved {
messageType = victoropsAlertStateRecovery
}
groupKey, err := notify.ExtractGroupKey(ctx)
if err != nil {
return false, err
}
bodyJSON := map[string]interface{}{
"message_type": messageType,
"entity_id": groupKey.Hash(),
"entity_display_name": tmpl(DefaultMessageTitleEmbed),
"timestamp": time.Now().Unix(),
"state_message": tmpl(DefaultMessageEmbed),
"monitoring_tool": "Grafana v" + setting.BuildVersion,
}
if tmplErr != nil {
vn.log.Warn("failed to expand message template. "+
"", "error", tmplErr.Error())
tmplErr = nil
}
_ = withStoredImages(ctx, vn.log, vn.images,
func(index int, image ngmodels.Image) error {
if image.URL != "" {
bodyJSON["image_url"] = image.URL
return ErrImagesDone
}
return nil
}, as...)
ruleURL := joinUrlPath(vn.tmpl.ExternalURL.String(), "/alerting/list", vn.log)
bodyJSON["alert_url"] = ruleURL
u := tmpl(vn.settings.URL)
if tmplErr != nil {
vn.log.Info("failed to expand URL template", "error", tmplErr.Error(), "fallback", vn.settings.URL)
u = vn.settings.URL
}
b, err := json.Marshal(bodyJSON)
if err != nil {
return false, err
}
cmd := &models.SendWebhookSync{
Url: u,
Body: string(b),
}
if err := vn.ns.SendWebhookSync(ctx, cmd); err != nil {
vn.log.Error("failed to send notification", "error", err, "webhook", vn.Name)
return false, err
}
return true, nil
}
func (vn *VictoropsNotifier) SendResolved() bool {
return !vn.GetDisableResolveMessage()
}