Alerting: Migrate Alertmanager notifier (#34304)

* Alerting: Port Alertmanager notifier to v8

Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>
This commit is contained in:
Arve Knudsen
2021-05-19 15:27:41 +02:00
committed by GitHub
parent 5a449d5963
commit 9dfaa037d1
6 changed files with 326 additions and 0 deletions

View File

@@ -424,6 +424,8 @@ func (am *Alertmanager) buildReceiverIntegrations(receiver *apimodels.PostableAp
n, err = channels.NewWebHookNotifier(cfg, tmpl)
case "sensugo":
n, err = channels.NewSensuGoNotifier(cfg, tmpl)
case "alertmanager":
n, err = channels.NewAlertmanagerNotifier(cfg, tmpl)
default:
return nil, fmt.Errorf("notifier %s is not supported", r.Type)
}

View File

@@ -404,5 +404,21 @@ func GetAvailableNotifiers() []*alerting.NotifierPlugin {
},
},
},
{
Type: "alertmanager",
Name: "Alertmanager",
Description: "Sends notifications to Alertmanager",
Heading: "Alertmanager Settings",
Options: []alerting.NotifierOption{
{
Label: "URL",
Element: alerting.ElementTypeInput,
InputType: alerting.InputTypeText,
Placeholder: "http://localhost:9093",
PropertyName: "url",
Required: true,
},
},
},
}
}

View File

@@ -0,0 +1,106 @@
package channels
import (
"context"
"encoding/json"
"fmt"
"net/url"
"strings"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
old_notifiers "github.com/grafana/grafana/pkg/services/alerting/notifiers"
"github.com/prometheus/alertmanager/template"
"github.com/prometheus/alertmanager/types"
)
// NewAlertmanagerNotifier returns a new Alertmanager notifier.
func NewAlertmanagerNotifier(model *NotificationChannelConfig, t *template.Template) (*AlertmanagerNotifier, error) {
if model.Settings == nil {
return nil, alerting.ValidationError{Reason: "No settings supplied"}
}
urlStr := model.Settings.Get("url").MustString()
if urlStr == "" {
return nil, alerting.ValidationError{Reason: "Could not find url property in settings"}
}
var urls []*url.URL
for _, uS := range strings.Split(urlStr, ",") {
uS = strings.TrimSpace(uS)
if uS == "" {
continue
}
uS = strings.TrimSuffix(uS, "/") + "/api/v1/alerts"
u, err := url.Parse(uS)
if err != nil {
return nil, alerting.ValidationError{Reason: "Invalid url property in settings"}
}
urls = append(urls, u)
}
basicAuthUser := model.Settings.Get("basicAuthUser").MustString()
basicAuthPassword := model.DecryptedValue("basicAuthPassword", model.Settings.Get("basicAuthPassword").MustString())
return &AlertmanagerNotifier{
NotifierBase: old_notifiers.NewNotifierBase(&models.AlertNotification{
Uid: model.UID,
Name: model.Name,
DisableResolveMessage: model.DisableResolveMessage,
Settings: model.Settings,
}),
urls: urls,
basicAuthUser: basicAuthUser,
basicAuthPassword: basicAuthPassword,
logger: log.New("alerting.notifier.prometheus-alertmanager"),
}, nil
}
// AlertmanagerNotifier sends alert notifications to the alert manager
type AlertmanagerNotifier struct {
old_notifiers.NotifierBase
urls []*url.URL
basicAuthUser string
basicAuthPassword string
logger log.Logger
}
// Notify sends alert notifications to Alertmanager.
func (n *AlertmanagerNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) {
n.logger.Debug("Sending Alertmanager alert", "alertmanager", n.Name)
if len(as) == 0 {
return true, nil
}
body, err := json.Marshal(as)
if err != nil {
return false, err
}
errCnt := 0
for _, u := range n.urls {
if _, err := sendHTTPRequest(ctx, u, httpCfg{
user: n.basicAuthUser,
password: n.basicAuthPassword,
body: body,
}, n.logger); err != nil {
n.logger.Warn("Failed to send to Alertmanager", "error", err, "alertmanager", n.Name, "url", u.String())
errCnt++
}
}
if errCnt == len(n.urls) {
// All attempts to send alerts have failed
n.logger.Warn("All attempts to send to Alertmanager failed", "alertmanager", n.Name)
return false, fmt.Errorf("failed to send alert to Alertmanager")
}
return true, nil
}
func (n *AlertmanagerNotifier) SendResolved() bool {
return !n.GetDisableResolveMessage()
}

View File

@@ -0,0 +1,103 @@
package channels
import (
"context"
"encoding/json"
"net/url"
"testing"
"github.com/prometheus/alertmanager/notify"
"github.com/prometheus/alertmanager/types"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/alerting"
)
func TestAlertmanagerNotifier(t *testing.T) {
tmpl := templateForTests(t)
externalURL, err := url.Parse("http://localhost")
require.NoError(t, err)
tmpl.ExternalURL = externalURL
cases := []struct {
name string
settings string
alerts []*types.Alert
expInitError error
expMsgError error
}{
{
name: "Default config with one alert",
settings: `{"url": "https://alertmanager.com"}`,
alerts: []*types.Alert{
{
Alert: model.Alert{
Labels: model.LabelSet{"__alert_rule_uid__": "rule uid", "alertname": "alert1", "lbl1": "val1"},
Annotations: model.LabelSet{"ann1": "annv1"},
},
},
},
}, {
name: "Error in initing: missing URL",
settings: `{}`,
expInitError: alerting.ValidationError{Reason: "Could not find url property in settings"},
}, {
name: "Error in initing: invalid URL",
settings: `{
"url": "://alertmanager.com"
}`,
expInitError: alerting.ValidationError{Reason: "Invalid url property in settings"},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
settingsJSON, err := simplejson.NewJson([]byte(c.settings))
require.NoError(t, err)
m := &NotificationChannelConfig{
Name: "Alertmanager",
Type: "alertmanager",
Settings: settingsJSON,
}
sn, err := NewAlertmanagerNotifier(m, tmpl)
if c.expInitError != nil {
require.Error(t, err)
require.Equal(t, c.expInitError, err)
return
}
require.NoError(t, err)
var body []byte
origSendHTTPRequest := sendHTTPRequest
t.Cleanup(func() {
sendHTTPRequest = origSendHTTPRequest
})
sendHTTPRequest = func(ctx context.Context, url *url.URL, cfg httpCfg, logger log.Logger) ([]byte, error) {
body = cfg.body
return nil, nil
}
ctx := notify.WithGroupKey(context.Background(), "alertname")
ctx = notify.WithGroupLabels(ctx, model.LabelSet{"alertname": ""})
ok, err := sn.Notify(ctx, c.alerts...)
if c.expMsgError != nil {
require.False(t, ok)
require.Error(t, err)
require.Equal(t, c.expMsgError, err)
return
}
require.NoError(t, err)
require.True(t, ok)
expBody, err := json.Marshal(c.alerts)
require.NoError(t, err)
require.JSONEq(t, string(expBody), string(body))
})
}
}

View File

@@ -1,6 +1,18 @@
package channels
import (
"bytes"
"context"
"crypto/tls"
"fmt"
"io"
"net"
"net/http"
"net/url"
"time"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/util"
"github.com/prometheus/common/model"
"github.com/grafana/grafana/pkg/components/securejsondata"
@@ -36,3 +48,65 @@ func (an *NotificationChannelConfig) DecryptedValue(field string, fallback strin
}
return fallback
}
type httpCfg struct {
body []byte
user string
password string
}
// sendHTTPRequest sends an HTTP request.
// Stubbable by tests.
var sendHTTPRequest = func(ctx context.Context, url *url.URL, cfg httpCfg, logger log.Logger) ([]byte, error) {
var reader io.Reader
if len(cfg.body) > 0 {
reader = bytes.NewReader(cfg.body)
}
request, err := http.NewRequestWithContext(ctx, http.MethodPost, url.String(), reader)
if err != nil {
return nil, fmt.Errorf("failed to create HTTP request: %w", err)
}
if cfg.user != "" && cfg.password != "" {
request.Header.Set("Authorization", util.GetBasicAuthHeader(cfg.user, cfg.password))
}
request.Header.Set("Content-Type", "application/json")
request.Header.Set("User-Agent", "Grafana")
netTransport := &http.Transport{
TLSClientConfig: &tls.Config{
Renegotiation: tls.RenegotiateFreelyAsClient,
},
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
}).DialContext,
TLSHandshakeTimeout: 5 * time.Second,
}
netClient := &http.Client{
Timeout: time.Second * 30,
Transport: netTransport,
}
resp, err := netClient.Do(request)
if err != nil {
return nil, err
}
defer func() {
if err := resp.Body.Close(); err != nil {
logger.Warn("Failed to close response body", "err", err)
}
}()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}
if resp.StatusCode/100 != 2 {
logger.Warn("HTTP request failed", "url", request.URL.String(), "statusCode", resp.Status, "body",
string(respBody))
return nil, fmt.Errorf("failed to send HTTP request - status code %d", resp.StatusCode)
}
logger.Debug("Sending HTTP request succeeded", "url", request.URL.String(), "statusCode", resp.Status)
return respBody, nil
}

View File

@@ -801,6 +801,31 @@ var expAvailableChannelJsonOutput = `
"secure": false
}
]
},
{
"type": "alertmanager",
"name": "Alertmanager",
"heading": "Alertmanager Settings",
"description": "Sends notifications to Alertmanager",
"info": "",
"options": [
{
"element": "input",
"inputType": "text",
"label": "URL",
"description": "",
"placeholder": "http://localhost:9093",
"propertyName": "url",
"selectOptions": null,
"showWhen": {
"field": "",
"is": ""
},
"required": true,
"validationRule": "",
"secure": false
}
]
}
]
`