Skip to content

Commit

Permalink
feat: webhook template
Browse files Browse the repository at this point in the history
  • Loading branch information
baerwang committed Jan 21, 2025
1 parent 3b06b97 commit ccb5f43
Show file tree
Hide file tree
Showing 6 changed files with 364 additions and 30 deletions.
41 changes: 25 additions & 16 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,9 @@ func resolveFilepaths(baseDir string, cfg *Config) {
for _, cfg := range receiver.WebhookConfigs {
cfg.HTTPConfig.SetDirectory(baseDir)
}
for _, cfg := range receiver.WebHookTemplateConfigs {
cfg.HTTPConfig.SetDirectory(baseDir)
}
for _, cfg := range receiver.WechatConfigs {
cfg.HTTPConfig.SetDirectory(baseDir)
}
Expand Down Expand Up @@ -412,6 +415,11 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
wh.HTTPConfig = c.Global.HTTPConfig
}
}
for _, wh := range rcv.WebHookTemplateConfigs {
if wh.HTTPConfig == nil {
wh.HTTPConfig = c.Global.HTTPConfig
}
}
for _, ec := range rcv.EmailConfigs {
if ec.TLSConfig == nil {
ec.TLSConfig = c.Global.SMTPTLSConfig
Expand Down Expand Up @@ -1010,22 +1018,23 @@ type Receiver struct {
// A unique identifier for this receiver.
Name string `yaml:"name" json:"name"`

DiscordConfigs []*DiscordConfig `yaml:"discord_configs,omitempty" json:"discord_configs,omitempty"`
EmailConfigs []*EmailConfig `yaml:"email_configs,omitempty" json:"email_configs,omitempty"`
PagerdutyConfigs []*PagerdutyConfig `yaml:"pagerduty_configs,omitempty" json:"pagerduty_configs,omitempty"`
SlackConfigs []*SlackConfig `yaml:"slack_configs,omitempty" json:"slack_configs,omitempty"`
WebhookConfigs []*WebhookConfig `yaml:"webhook_configs,omitempty" json:"webhook_configs,omitempty"`
OpsGenieConfigs []*OpsGenieConfig `yaml:"opsgenie_configs,omitempty" json:"opsgenie_configs,omitempty"`
WechatConfigs []*WechatConfig `yaml:"wechat_configs,omitempty" json:"wechat_configs,omitempty"`
PushoverConfigs []*PushoverConfig `yaml:"pushover_configs,omitempty" json:"pushover_configs,omitempty"`
VictorOpsConfigs []*VictorOpsConfig `yaml:"victorops_configs,omitempty" json:"victorops_configs,omitempty"`
SNSConfigs []*SNSConfig `yaml:"sns_configs,omitempty" json:"sns_configs,omitempty"`
TelegramConfigs []*TelegramConfig `yaml:"telegram_configs,omitempty" json:"telegram_configs,omitempty"`
WebexConfigs []*WebexConfig `yaml:"webex_configs,omitempty" json:"webex_configs,omitempty"`
MSTeamsConfigs []*MSTeamsConfig `yaml:"msteams_configs,omitempty" json:"msteams_configs,omitempty"`
MSTeamsV2Configs []*MSTeamsV2Config `yaml:"msteamsv2_configs,omitempty" json:"msteamsv2_configs,omitempty"`
JiraConfigs []*JiraConfig `yaml:"jira_configs,omitempty" json:"jira_configs,omitempty"`
RocketchatConfigs []*RocketchatConfig `yaml:"rocketchat_configs,omitempty" json:"rocketchat_configs,omitempty"`
DiscordConfigs []*DiscordConfig `yaml:"discord_configs,omitempty" json:"discord_configs,omitempty"`
EmailConfigs []*EmailConfig `yaml:"email_configs,omitempty" json:"email_configs,omitempty"`
PagerdutyConfigs []*PagerdutyConfig `yaml:"pagerduty_configs,omitempty" json:"pagerduty_configs,omitempty"`
SlackConfigs []*SlackConfig `yaml:"slack_configs,omitempty" json:"slack_configs,omitempty"`
WebhookConfigs []*WebhookConfig `yaml:"webhook_configs,omitempty" json:"webhook_configs,omitempty"`
WebHookTemplateConfigs []*WebhookTemplateConfig `yaml:"webhook_template_configs,omitempty" json:"webhook_template_configs,omitempty"`
OpsGenieConfigs []*OpsGenieConfig `yaml:"opsgenie_configs,omitempty" json:"opsgenie_configs,omitempty"`
WechatConfigs []*WechatConfig `yaml:"wechat_configs,omitempty" json:"wechat_configs,omitempty"`
PushoverConfigs []*PushoverConfig `yaml:"pushover_configs,omitempty" json:"pushover_configs,omitempty"`
VictorOpsConfigs []*VictorOpsConfig `yaml:"victorops_configs,omitempty" json:"victorops_configs,omitempty"`
SNSConfigs []*SNSConfig `yaml:"sns_configs,omitempty" json:"sns_configs,omitempty"`
TelegramConfigs []*TelegramConfig `yaml:"telegram_configs,omitempty" json:"telegram_configs,omitempty"`
WebexConfigs []*WebexConfig `yaml:"webex_configs,omitempty" json:"webex_configs,omitempty"`
MSTeamsConfigs []*MSTeamsConfig `yaml:"msteams_configs,omitempty" json:"msteams_configs,omitempty"`
MSTeamsV2Configs []*MSTeamsV2Config `yaml:"msteamsv2_configs,omitempty" json:"msteamsv2_configs,omitempty"`
JiraConfigs []*JiraConfig `yaml:"jira_configs,omitempty" json:"jira_configs,omitempty"`
RocketchatConfigs []*RocketchatConfig `yaml:"rocketchat_configs,omitempty" json:"rocketchat_configs,omitempty"`
}

// UnmarshalYAML implements the yaml.Unmarshaler interface for Receiver.
Expand Down
40 changes: 40 additions & 0 deletions config/notifiers.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ var (
},
}

// DefaultWebhookTemplateConfig defines default values for Webhook template configurations.
DefaultWebhookTemplateConfig = WebhookTemplateConfig{
NotifierConfig: NotifierConfig{
VSendResolved: true,
},
}

// DefaultWebexConfig defines default values for Webex configurations.
DefaultWebexConfig = WebexConfig{
NotifierConfig: NotifierConfig{
Expand Down Expand Up @@ -557,6 +564,39 @@ func (c *WebhookConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
return nil
}

// WebhookTemplateConfig configures notifications via a generic webhook.
type WebhookTemplateConfig struct {
NotifierConfig `yaml:",inline" json:",inline"`

HTTPConfig *commoncfg.HTTPClientConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"`

// URL to send POST request to.
URL *SecretURL `yaml:"url" json:"url"`
URLFile string `yaml:"url_file" json:"url_file"`

Template string `yaml:"template,omitempty" json:"template,omitempty"`

// 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"`
}

// UnmarshalYAML implements the yaml.Unmarshaler interface.
func (c *WebhookTemplateConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
*c = DefaultWebhookTemplateConfig
type plain WebhookTemplateConfig
if err := unmarshal((*plain)(c)); err != nil {
return err
}
if c.URL == nil && c.URLFile == "" {
return errors.New("one of url or url_file must be configured")
}
if c.URL != nil && c.URLFile != "" {
return errors.New("at most one of url & url_file must be configured")
}
return nil
}

// WechatConfig configures notifications via Wechat.
type WechatConfig struct {
NotifierConfig `yaml:",inline" json:",inline"`
Expand Down
4 changes: 4 additions & 0 deletions config/receiver/receiver.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import (
"github.com/prometheus/alertmanager/notify/victorops"
"github.com/prometheus/alertmanager/notify/webex"
"github.com/prometheus/alertmanager/notify/webhook"
"github.com/prometheus/alertmanager/notify/webhook_template"
"github.com/prometheus/alertmanager/notify/wechat"
"github.com/prometheus/alertmanager/template"
"github.com/prometheus/alertmanager/types"
Expand Down Expand Up @@ -64,6 +65,9 @@ func BuildReceiverIntegrations(nc config.Receiver, tmpl *template.Template, logg
for i, c := range nc.WebhookConfigs {
add("webhook", i, c, func(l *slog.Logger) (notify.Notifier, error) { return webhook.New(c, tmpl, l, httpOpts...) })
}
for i, c := range nc.WebHookTemplateConfigs {
add("webhook_template", i, c, func(l *slog.Logger) (notify.Notifier, error) { return webhook_template.New(c, tmpl, l, httpOpts...) })
}
for i, c := range nc.EmailConfigs {
add("email", i, c, func(l *slog.Logger) (notify.Notifier, error) { return email.New(c, tmpl, l), nil })
}
Expand Down
65 changes: 51 additions & 14 deletions config/receiver/receiver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,21 +68,58 @@ func TestBuildReceiverIntegrations(t *testing.T) {
},
err: true,
},
{
receiver: config.Receiver{
Name: "foo",
WebHookTemplateConfigs: []*config.WebhookTemplateConfig{
{
HTTPConfig: &commoncfg.HTTPClientConfig{},
},
{
HTTPConfig: &commoncfg.HTTPClientConfig{},
NotifierConfig: config.NotifierConfig{
VSendResolved: true,
},
},
},
},
exp: []notify.Integration{
notify.NewIntegration(nil, sendResolved(false), "webhook_template", 0, "foo"),
notify.NewIntegration(nil, sendResolved(true), "webhook_template", 1, "foo"),
},
},
{
receiver: config.Receiver{
Name: "foo",
WebHookTemplateConfigs: []*config.WebhookTemplateConfig{
{
HTTPConfig: &commoncfg.HTTPClientConfig{
TLSConfig: commoncfg.TLSConfig{
CAFile: "not_existing",
},
},
},
},
},
err: true,
},
} {
tc := tc
t.Run("", func(t *testing.T) {
integrations, err := BuildReceiverIntegrations(tc.receiver, nil, nil)
if tc.err {
require.Error(t, err)
return
}
require.NoError(t, err)
require.Len(t, integrations, len(tc.exp))
for i := range tc.exp {
require.Equal(t, tc.exp[i].SendResolved(), integrations[i].SendResolved())
require.Equal(t, tc.exp[i].Name(), integrations[i].Name())
require.Equal(t, tc.exp[i].Index(), integrations[i].Index())
}
})
t.Run(
"", func(t *testing.T) {
integrations, err := BuildReceiverIntegrations(tc.receiver, nil, nil)
if tc.err {
require.Error(t, err)
return
}
require.NoError(t, err)
require.Len(t, integrations, len(tc.exp))
for i := range tc.exp {
require.Equal(t, tc.exp[i].SendResolved(), integrations[i].SendResolved())
require.Equal(t, tc.exp[i].Name(), integrations[i].Name())
require.Equal(t, tc.exp[i].Index(), integrations[i].Index())
}
},
)
}
}
112 changes: 112 additions & 0 deletions notify/webhook_template/webhook_template.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// Copyright 2019 Prometheus Team
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package webhook_template

import (
"bytes"
"context"
"encoding/json"
"fmt"
"log/slog"
"net/http"
"os"
"strings"

commoncfg "github.com/prometheus/common/config"

"github.com/prometheus/alertmanager/config"
"github.com/prometheus/alertmanager/notify"
"github.com/prometheus/alertmanager/template"
"github.com/prometheus/alertmanager/types"
)

// Notifier implements a Notifier for generic webhooks.
type Notifier struct {
conf *config.WebhookTemplateConfig
tmpl *template.Template
logger *slog.Logger
client *http.Client
retrier *notify.Retrier
}

// New returns a new Webhook.
func New(conf *config.WebhookTemplateConfig, t *template.Template, l *slog.Logger, httpOpts ...commoncfg.HTTPClientOption) (*Notifier, error) {
client, err := commoncfg.NewClientFromConfig(*conf.HTTPConfig, "webhook_template", httpOpts...)
if err != nil {
return nil, err
}
return &Notifier{
conf: conf,
tmpl: t,
logger: l,
client: client,
// Webhooks are assumed to respond with 2xx response codes on a successful
// request and 5xx response codes are assumed to be recoverable.
retrier: &notify.Retrier{},
}, nil
}

// Notify implements the Notifier interface.
func (n *Notifier) Notify(ctx context.Context, alerts ...*types.Alert) (bool, error) {
key, err := notify.ExtractGroupKey(ctx)
if err != nil {
return false, err
}
n.logger.Debug("extracted group key", "key", key)

data := notify.GetTemplateData(ctx, n.tmpl, alerts, n.logger)
tmpl := notify.TmplText(n.tmpl, data, &err)
if err != nil {
return false, err
}

body := tmpl(n.conf.Template)

var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(body); err != nil {
return false, err
}

var url string
if n.conf.URL != nil {
url = n.conf.URL.String()
} else {
content, err := os.ReadFile(n.conf.URLFile)
if err != nil {
return false, fmt.Errorf("read url_file: %w", err)
}
url = strings.TrimSpace(string(content))
}

if n.conf.Timeout > 0 {
postCtx, cancel := context.WithTimeoutCause(ctx, n.conf.Timeout, fmt.Errorf("configured webhook template timeout reached (%s)", n.conf.Timeout))
defer cancel()
ctx = postCtx
}

resp, err := notify.PostJSON(ctx, n.client, url, &buf)
if err != nil {
if ctx.Err() != nil {
err = fmt.Errorf("%w: %w", err, context.Cause(ctx))
}
return true, notify.RedactURL(err)
}
defer notify.Drain(resp)

shouldRetry, err := n.retrier.Check(resp.StatusCode, resp.Body)
if err != nil {
return shouldRetry, notify.NewErrorWithReason(notify.GetFailureReasonFromStatusCode(resp.StatusCode), err)
}
return shouldRetry, err
}
Loading

0 comments on commit ccb5f43

Please sign in to comment.