Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion config/notifiers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
9 changes: 9 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -1840,6 +1840,15 @@ url_file: <filepath>
# no timeout should be applied.
# NOTE: This will have no effect if set higher than the group_interval.
[ timeout: <duration> | 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: { <string>: <tmpl_string>, ... } ]
```

The Alertmanager
Expand Down
33 changes: 33 additions & 0 deletions notify/webhook/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
}
126 changes: 126 additions & 0 deletions notify/webhook/webhook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@ package webhook
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/url"
"os"
"testing"
"time"
Expand Down Expand Up @@ -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))
}