Skip to content
Closed
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/data/
/alertmanager
/alertmanager.exe
*.yml
*.yaml
/.build
Expand Down
1 change: 1 addition & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,7 @@ type Receiver struct {
OpsGenieConfigs []*OpsGenieConfig `yaml:"opsgenie_configs,omitempty" json:"opsgenie_configs,omitempty"`
PushoverConfigs []*PushoverConfig `yaml:"pushover_configs,omitempty" json:"pushover_configs,omitempty"`
VictorOpsConfigs []*VictorOpsConfig `yaml:"victorops_configs,omitempty" json:"victorops_configs,omitempty"`
AmazonSNSConfigs []*AmazonSNSConfig `yaml:"amazon_sns_configs,omitempty" json:"amazon_sns_configs,omitempty"`

// Catches all undefined fields and must be empty after parsing.
XXX map[string]interface{} `yaml:",inline" json:"-"`
Expand Down
35 changes: 35 additions & 0 deletions config/notifiers.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,15 @@ var (
Retry: duration(1 * time.Minute),
Expire: duration(1 * time.Hour),
}

// DefaultAmazonSNSConfig defines default values for Amazon SNS configurations.
DefaultAmazonSNSConfig = AmazonSNSConfig{
NotifierConfig: NotifierConfig{
VSendResolved: true,
},
Subject: `{{ template "amazon_sns.default.subject" . }}`,
Message: `{{ template "amazon_sns.default.message" . }}`,
}
)

// NotifierConfig contains base options common across all notifier configurations.
Expand Down Expand Up @@ -392,3 +401,29 @@ func (c *PushoverConfig) UnmarshalYAML(unmarshal func(interface{}) error) error
}
return checkOverflow(c.XXX, "pushover config")
}

// AmazonSNSConfig configures notifications via Amazon SNS.
type AmazonSNSConfig struct {
NotifierConfig `yaml:",inline" json:",inline"`

AWSRegion string `yaml:"aws_region" json:"aws_region"`
TopicARN string `yaml:"topic_arn" json:"topic_arn"`
Subject string `yaml:"subject" json:"subject"`
Message string `yaml:"message" json:"message"`

XXX map[string]interface{} `yaml:",inline" json:"-"`
}

// UnmarshalYAML implements the yaml.Unmarshaler interface.
func (c *AmazonSNSConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
*c = DefaultAmazonSNSConfig
type plain AmazonSNSConfig
if err := unmarshal((*plain)(c)); err != nil {
return err
}
// Allow blank region, but not missing ARN
if c.TopicARN == "" {
return fmt.Errorf("missing topic_arn key in amazon_sns_config")
}
return checkOverflow(c.XXX, "amazon_sns_config")
}
113 changes: 113 additions & 0 deletions notify/amazonsns_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// Copyright 2015 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 notify

import (
"errors"
"github.com/stretchr/testify/require"
"golang.org/x/net/context"
"net/url"
"testing"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/sns"

"github.com/prometheus/alertmanager/config"
"github.com/prometheus/alertmanager/template"
"github.com/prometheus/alertmanager/types"
"github.com/prometheus/common/model"
)

func dummySNSSetup(t *testing.T) (context.Context, []*types.Alert, *AmazonSNS) {
ctx := WithReceiverName(context.Background(), "name")
lset := model.LabelSet{
"group_label_key": "group_label_value",
}
ctx = WithGroupLabels(ctx, lset)

alerts := []*types.Alert{{}, {}}

c := &config.AmazonSNSConfig{
TopicARN: "arn:aws:sns:no-region:1234567890:topic",
Subject: `{{ template "amazon_sns.default.subject" . }}`,
Message: `{{ template "amazon_sns.default.message" . }}`,
}
tmpl, err := template.FromGlobs()
require.NoError(t, err, "Failed template setup")
tmpl.ExternalURL, err = url.Parse("http://localhost/")
require.NoError(t, err, "Failed template URL setup")

n := NewAmazonSNS(c, tmpl)

return ctx, alerts, n
}

func TestAmazonSNSIntegration(t *testing.T) {
ctx, alerts, n := dummySNSSetup(t)

n.testPublisher = func(c *aws.Config, pi *sns.PublishInput) error {
require.Contains(t, aws.StringValue(c.Region), "no-region", "AWS Config not set to correct region")
subj := aws.StringValue(pi.Subject)
require.Contains(t, subj, "[FIRING:2]", "Default Subject missing alerts raised summary")
require.Contains(t, subj, "group_label_value", "Default Subject missing Context label")
mess := aws.StringValue(pi.Message)
require.Contains(t, mess, "Source", "Default Message missing structure")
return nil
}

retry, err := n.Notify(ctx, alerts...)
require.NoError(t, err, "Happy path")
require.Equal(t, false, retry, "Happy path no need to retry")
}

func TestAmazonSNSBadARN(t *testing.T) {
ctx, alerts, n := dummySNSSetup(t)

n.conf.TopicARN = "fdsfds"
n.testPublisher = func(c *aws.Config, pi *sns.PublishInput) error {
t.Fatal("Shouldn't attempt to publish to a bad TopicARN")
return nil
}

retry, err := n.Notify(ctx, alerts...)
require.Error(t, err, "Bad ARN should error")
require.Contains(t, err.Error(), "fdsfds", "Incorrect bad ARN message")
require.Equal(t, false, retry, "Bad ARN shouldn't retry")
}

func TestAmazonSNSRegionOverride(t *testing.T) {
ctx, alerts, n := dummySNSSetup(t)

n.conf.AWSRegion = "somewhere"
n.testPublisher = func(c *aws.Config, pi *sns.PublishInput) error {
require.Contains(t, aws.StringValue(c.Region), "somewhere", "Failed to override AWS Region")
return nil
}

retry, err := n.Notify(ctx, alerts...)
require.NoError(t, err, "SNS Override should work")
require.Equal(t, false, retry, "SNS Override no need to retry")
}

func TestAmazonSNSPublishFailRetry(t *testing.T) {
ctx, alerts, n := dummySNSSetup(t)

n.testPublisher = func(c *aws.Config, pi *sns.PublishInput) error {
return errors.New("Dummy failure")
}

retry, err := n.Notify(ctx, alerts...)
require.Error(t, err, "Publish error should propagate")
require.Contains(t, err.Error(), "Dummy failure", "Need to see the error message from SNS publish upon failure")
require.Equal(t, true, retry, "Should retry on publish failure")
}
75 changes: 75 additions & 0 deletions notify/impl.go
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ import (
"strings"
"time"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/sns"

"github.com/prometheus/common/log"
"github.com/prometheus/common/model"
"golang.org/x/net/context"
Expand Down Expand Up @@ -129,6 +133,10 @@ func BuildReceiverIntegrations(nc *config.Receiver, tmpl *template.Template) []I
n := NewPushover(c, tmpl)
add("pushover", i, n, c)
}
for i, c := range nc.AmazonSNSConfigs {
n := NewAmazonSNS(c, tmpl)
add("amazon_sns", i, n, c)
}
return integrations
}

Expand Down Expand Up @@ -934,6 +942,73 @@ func (n *Pushover) Notify(ctx context.Context, as ...*types.Alert) (bool, error)
return false, nil
}

// AmazonSNS implements a Notifier for Amazon SNS notifications.
type AmazonSNS struct {
conf *config.AmazonSNSConfig
tmpl *template.Template

// For testing only
testPublisher func(*aws.Config, *sns.PublishInput) error
}

// NewAmazonSNS returns a new AmazonSNS notifier.
func NewAmazonSNS(c *config.AmazonSNSConfig, t *template.Template) *AmazonSNS {
return &AmazonSNS{
conf: c,
tmpl: t,
testPublisher: nil,
}
}

// Notify implements the Notifier interface.
func (n *AmazonSNS) Notify(ctx context.Context, as ...*types.Alert) (bool, error) {
var err error
var (
data = n.tmpl.Data(receiverName(ctx), groupLabels(ctx), as...)
tmpl = tmplText(n.tmpl, data, &err)
region = tmpl(n.conf.AWSRegion)
topicARN = tmpl(n.conf.TopicARN)
subject = tmpl(n.conf.Subject)
message = tmpl(n.conf.Message)
)
if err != nil {
return false, fmt.Errorf("Amazon SNS templating error: %s", err)
}

// SNS ARNs are of the form arn:aws:sns:<region>:<account-id>:<topicname>
parts := strings.SplitN(topicARN, ":", 6)
if len(parts) < 6 {
return false, fmt.Errorf("Amazon SNS topic_arn not a valid ARN: '%s'", topicARN)
}
if region == "" {
region = parts[3]
}
awsConfig := aws.NewConfig()
if region != "" {
awsConfig = awsConfig.WithRegion(region)
}

params := &sns.PublishInput{
Message: aws.String(message),
MessageStructure: aws.String("string"),
Subject: aws.String(subject),
TopicArn: aws.String(topicARN),
}

if n.testPublisher != nil {
err = n.testPublisher(awsConfig, params)
} else {
snsAPI := sns.New(session.New(awsConfig))
_, err = snsAPI.Publish(params)
}

if err != nil {
return true, fmt.Errorf("Amazon SNS publishing error: %s", err)
}

return false, nil
}

func tmplText(tmpl *template.Template, data *template.Data, err *error) func(string) string {
return func(name string) (s string) {
if *err != nil {
Expand Down
12 changes: 12 additions & 0 deletions template/default.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -190,3 +190,15 @@ Alerts Resolved:
{{ end }}
{{ end }}
{{ define "pushover.default.url" }}{{ template "__alertmanagerURL" . }}{{ end }}

{{ define "amazon_sns.default.subject" }}{{ template "__subject" . }}{{ end }}
{{ define "amazon_sns.default.message" }}{{ .CommonAnnotations.SortedPairs.Values | join " " }}
{{ if gt (len .Alerts.Firing) 0 }}
Alerts Firing:
{{ template "__text_alert_list" .Alerts.Firing }}
{{ end }}
{{ if gt (len .Alerts.Resolved) 0 }}
Alerts Resolved:
{{ template "__text_alert_list" .Alerts.Resolved }}
{{ end }}
{{ end }}
4 changes: 2 additions & 2 deletions template/internal/deftmpl/bindata.go

Large diffs are not rendered by default.

Loading