mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
106
pkg/services/ngalert/notifier/channels/alertmanager.go
Normal file
106
pkg/services/ngalert/notifier/channels/alertmanager.go
Normal 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()
|
||||
}
|
||||
103
pkg/services/ngalert/notifier/channels/alertmanager_test.go
Normal file
103
pkg/services/ngalert/notifier/channels/alertmanager_test.go
Normal 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))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
`
|
||||
|
||||
Reference in New Issue
Block a user