diff --git a/config/notifiers.go b/config/notifiers.go index 3da2e9cf69..5e07a64f4e 100644 --- a/config/notifiers.go +++ b/config/notifiers.go @@ -653,7 +653,8 @@ type WebhookConfig struct { // Timeout is the maximum time allowed to invoke the webhook. Setting this to 0 // does not impose a timeout. - Timeout time.Duration `yaml:"timeout" json:"timeout"` + Timeout time.Duration `yaml:"timeout" json:"timeout"` + Payload map[string]any `yaml:"payload,omitempty" json:"payload,omitempty"` } // UnmarshalYAML implements the yaml.Unmarshaler interface. diff --git a/docs/configuration.md b/docs/configuration.md index 837035e671..ea2e090dc2 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1840,6 +1840,15 @@ url_file: # no timeout should be applied. # NOTE: This will have no effect if set higher than the group_interval. [ timeout: | default = 0s ] + +# Define custom payload to be sent to the webhook endpoint. +# USE AT YOUR OWN RISK: This is an advanced configuration option that allows you +# to define a custom payload using Go templates. Be aware that the Alertmanager does not +# perform any validation on the resulting payload, and it is your responsibility to +# ensure that the generated payload is in the desired format expected by the receiving endpoint. +# The payload has to be valid JSON. You can use the `toJson` function to help with this. +# THE ALERTMANAGER TEAM WILL NOT PROVIDE ANY SUPPORT FOR ISSUES ARISING FROM THE USE OF THIS OPTION. +[ payload: { : , ... } ] ``` The Alertmanager diff --git a/notify/webhook/webhook.go b/notify/webhook/webhook.go index d35e407981..4165e28188 100644 --- a/notify/webhook/webhook.go +++ b/notify/webhook/webhook.go @@ -101,6 +101,14 @@ func (n *Notifier) Notify(ctx context.Context, alerts ...*types.Alert) (bool, er return false, err } + // Override the payload if a custom one is configured. + if n.conf.Payload != nil { + buf, err = n.renderPayload(msg) + if err != nil { + return false, fmt.Errorf("failed to render custom payload: %w", err) + } + } + var url string var tmplErr error tmpl := notify.TmplText(n.tmpl, data, &tmplErr) @@ -144,3 +152,28 @@ func (n *Notifier) Notify(ctx context.Context, alerts ...*types.Alert) (bool, er } return shouldRetry, err } + +func (n *Notifier) renderPayload( + data *Message, +) (bytes.Buffer, error) { + var ( + tmplTextErr error + tmplText = notify.TmplText(n.tmpl, data.Data, &tmplTextErr) + tmplTextFunc = func(tmpl string) (string, error) { + return tmplText(tmpl), tmplTextErr + } + ) + var err error + rendered := make(map[string]any, len(n.conf.Payload)) + for k, v := range n.conf.Payload { + rendered[k], err = template.DeepCopyWithTemplate(v, tmplTextFunc) + if err != nil { + return bytes.Buffer{}, err + } + } + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(rendered); err != nil { + return bytes.Buffer{}, err + } + return buf, nil +} diff --git a/notify/webhook/webhook_test.go b/notify/webhook/webhook_test.go index e96ad248a9..dfa73d47c0 100644 --- a/notify/webhook/webhook_test.go +++ b/notify/webhook/webhook_test.go @@ -16,10 +16,12 @@ package webhook import ( "bytes" "context" + "encoding/json" "fmt" "io" "net/http" "net/http/httptest" + "net/url" "os" "testing" "time" @@ -225,3 +227,127 @@ func TestWebhookURLTemplating(t *testing.T) { }) } } + +type roundTripFunc func(req *http.Request) *http.Response + +func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { + return f(req), nil +} + +// TestWebhookDefaultPayload tests that the default payload sent by the webhook notifier matches +// the behaviour before introducing templating. +func TestWebhookDefaultPayload(t *testing.T) { + var capturedPayload []byte + + mockTransport := roundTripFunc(func(req *http.Request) *http.Response { + var err error + capturedPayload, err = io.ReadAll(req.Body) + if err != nil { + t.Fatal(err) + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: http.NoBody, + } + }) + + u, err := url.Parse("http://localhost") + require.NoError(t, err) + + conf := &config.WebhookConfig{ + URL: config.SecretTemplateURL(u.String()), + HTTPConfig: &commoncfg.HTTPClientConfig{}, + } + + alerts := []*types.Alert{ + { + Alert: model.Alert{ + Labels: model.LabelSet{"alertname": "TestAlert"}, + Annotations: model.LabelSet{"summary": "Test summary"}, + StartsAt: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), + EndsAt: time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC), + GeneratorURL: "http://generator.url", + }, + }, + } + tmpl := test.CreateTmpl(t) + ctx := notify.WithGroupKey(context.Background(), "{}:{alertname=\"test1\"}") + ctx = notify.WithReceiverName(ctx, "test_receiver") + data := notify.GetTemplateData(ctx, tmpl, alerts, promslog.NewNopLogger()) + + msg := &Message{ + Version: "4", + Data: data, + GroupKey: "{}:{alertname=\"test1\"}", + } + + var buf bytes.Buffer + json.NewEncoder(&buf).Encode(msg) + n, err := New(conf, tmpl, promslog.NewNopLogger()) + require.NoError(t, err) + n.client.Transport = mockTransport + _, err = n.Notify(ctx, alerts...) + require.NoError(t, err) + + require.NotEmpty(t, capturedPayload) + require.JSONEq(t, buf.String(), string(capturedPayload)) +} + +func TestWebhookCustomPayload(t *testing.T) { + var capturedPayload []byte + + mockTransport := roundTripFunc(func(req *http.Request) *http.Response { + var err error + capturedPayload, err = io.ReadAll(req.Body) + if err != nil { + t.Fatal(err) + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: http.NoBody, + } + }) + + u, err := url.Parse("http://localhost") + require.NoError(t, err) + + conf := &config.WebhookConfig{ + URL: config.SecretTemplateURL(u.String()), + HTTPConfig: &commoncfg.HTTPClientConfig{}, + Payload: map[string]any{ + "custom": `some custom content`, + "commonLabels": "{{ .CommonLabels | toJson }}", + }, + } + + alerts := []*types.Alert{ + { + Alert: model.Alert{ + Labels: model.LabelSet{"alertname": "TestAlert"}, + Annotations: model.LabelSet{"summary": "Test summary"}, + StartsAt: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), + EndsAt: time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC), + GeneratorURL: "http://generator.url", + }, + }, + } + tmpl := test.CreateTmpl(t) + ctx := notify.WithGroupKey(context.Background(), "{}:{alertname=\"test1\"}") + ctx = notify.WithReceiverName(ctx, "test_receiver") + + msg := map[string]any{ + "custom": `some custom content`, + "commonLabels": map[string]string{"alertname": "TestAlert"}, + } + + var buf bytes.Buffer + json.NewEncoder(&buf).Encode(msg) + n, err := New(conf, tmpl, promslog.NewNopLogger()) + require.NoError(t, err) + n.client.Transport = mockTransport + _, err = n.Notify(ctx, alerts...) + require.NoError(t, err) + + require.NotEmpty(t, capturedPayload) + require.JSONEq(t, buf.String(), string(capturedPayload)) +}