Skip to content

Commit

Permalink
Merge pull request #1 from AndreZiviani/ignore-notifications
Browse files Browse the repository at this point in the history
feat: Add option to allow ignore some of the notifications
  • Loading branch information
AndreZiviani authored Apr 9, 2023
2 parents 21b136f + f9d7aa8 commit 719ba22
Show file tree
Hide file tree
Showing 6 changed files with 184 additions and 51 deletions.
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,46 @@ You must specify, at least, the following parameters via command options or envi
--slack-channel value Slack channel id [$SLACK_CHANNEL]
```

## Ignoring alerts

There are three flags that allows you to suppress an event, all of them can be used simultaneously:
* `--ignore-events`: Ignore all notifications of the specified event types.
* `--ignore-resources`: Ignore all notifications related to the specified resource, note that the notification will only be suppressed
if all of its resources are ignored.
* `--ignore-resource-event`: Ignore only the specified event type of that specific resource, format `<event type>:<resource identifier>`

All options allows multiple resources/events to be specified by using comma separated values:
```
--ignore-events "AWS_ELASTICACHE_BEFORE_UPDATE_DUE_NOTIFICATION,AWS_VPN_SINGLE_TUNNEL_NOTIFICATION"
--ignore-resources "elasticache-0,elasticache-1"
--ignore-resource-event "AWS_ELASTICACHE_BEFORE_UPDATE_DUE_NOTIFICATION:elasticache-0,AWS_VPN_SINGLE_TUNNEL_NOTIFICATION:vpn-01234567890abcdef"
```

Unfortunately (AFAIK) theres no documentation for all of the event types and resource identifiers (sometimes this is the ARN but
other times it is the resource name), I suggest extracting them from the Slack message.

Elasticache update example:
```
Event ARN: arn:aws:health:us-east-1::event/ELASTICACHE/AWS_ELASTICACHE_BEFORE_UPDATE_DUE_NOTIFICATION/AWS_ELASTICACHE_BEFORE_UPDATE_DUE_NOTIFICATION-us-east-1-aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee
^
event type
Resource(s): elasticache-0, elasticache-1
^
resource identifier
```

VPN single tunnel example:
```
Event ARN: arn:aws:health:us-east-1::event/VPN/AWS_VPN_SINGLE_TUNNEL_NOTIFICATION/AWS_VPN_SINGLE_TUNNEL_NOTIFICATION-aaaaaaaaaaaa-us-east-1-2023-M04
^
event type
Resource(s): vpn-01234567890abcdef
^
resource identifier
```

## Helm chart

A helm chart is available [here][chart]
Expand Down
155 changes: 117 additions & 38 deletions exporter/health.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,51 +20,77 @@ func (m *Metrics) HealthOrganizationEnabled(ctx context.Context) bool {
return false
}

func (m Metrics) SendSlackNotification(events []HealthEvent) {
for _, e := range events {
func (m *Metrics) GetHealthEvents() []HealthEvent {
var events []HealthEvent

resources := m.extractResources(e.AffectedResources)
accounts := m.extractAccounts(e.AffectedAccounts)

service := *e.Event.Service
region := *e.Event.Region
status := e.Event.StatusCode

var text, color string
attachmentFields := []slack.AttachmentField{
{Title: "Account(s)", Value: accounts, Short: true},
{Title: "Resource(s)", Value: resources, Short: true},
{Title: "Service", Value: service, Short: true},
{Title: "Region", Value: region, Short: true},
{Title: "Start Time", Value: e.Event.StartTime.In(m.tz).String(), Short: true},
{Title: "Status", Value: string(status), Short: true},
{Title: "Event ARN", Value: fmt.Sprintf("`%s`", *e.Event.Arn), Short: false},
{Title: "Updates", Value: *e.EventDescription.LatestDescription, Short: false},
}
if m.organizationEnabled {
events = m.GetOrgEvents()
} else {
events = m.GetAccountEvents()
}

if status == healthTypes.EventStatusCodeClosed {
text = fmt.Sprintf(":heavy_check_mark:*[RESOLVED] The AWS Health issue with the %s service in the %s region is now resolved.*", service, region)
color = "18be52"
attachmentFields = append(attachmentFields[:6], attachmentFields[5:]...)
attachmentFields[5] = slack.AttachmentField{Title: "End Time", Value: e.Event.EndTime.In(m.tz).String(), Short: true}
} else {
text = fmt.Sprintf(":rotating_light:*[NEW] AWS Health reported an issue with the %s service in the %s region.*", service, region)
color = "danger"
for _, e := range events {
if ignoreEvents(m.ignoreEvents, *e.Event.EventTypeCode) {
continue
}

attachment := slack.Attachment{
Color: color,
Fields: attachmentFields,
if ignoreResources(m.ignoreResources, e.AffectedResources) {
// only ignore this event if all resources are ignored
continue
}

_, _, err := m.slackApi.PostMessage(
m.slackChannel,
slack.MsgOptionText(text, false),
slack.MsgOptionAttachments(attachment),
)
if err != nil {
panic(err.Error())
if ignoreResourceEvent(m.ignoreResourceEvent, e) {
continue
}

m.SendSlackNotification(e)
}

return events
}

func (m Metrics) SendSlackNotification(e HealthEvent) {
resources := m.extractResources(e.AffectedResources)
accounts := m.extractAccounts(e.AffectedAccounts)

service := *e.Event.Service
region := *e.Event.Region
status := e.Event.StatusCode

var text, color string
attachmentFields := []slack.AttachmentField{
{Title: "Account(s)", Value: accounts, Short: true},
{Title: "Resource(s)", Value: resources, Short: true},
{Title: "Service", Value: service, Short: true},
{Title: "Region", Value: region, Short: true},
{Title: "Start Time", Value: e.Event.StartTime.In(m.tz).String(), Short: true},
{Title: "Status", Value: string(status), Short: true},
{Title: "Event ARN", Value: fmt.Sprintf("`%s`", *e.Event.Arn), Short: false},
{Title: "Updates", Value: *e.EventDescription.LatestDescription, Short: false},
}

if status == healthTypes.EventStatusCodeClosed {
text = fmt.Sprintf(":heavy_check_mark:*[RESOLVED] The AWS Health issue with the %s service in the %s region is now resolved.*", service, region)
color = "18be52"
attachmentFields = append(attachmentFields[:6], attachmentFields[5:]...)
attachmentFields[5] = slack.AttachmentField{Title: "End Time", Value: e.Event.EndTime.In(m.tz).String(), Short: true}
} else {
text = fmt.Sprintf(":rotating_light:*[NEW] AWS Health reported an issue with the %s service in the %s region.*", service, region)
color = "danger"
}

attachment := slack.Attachment{
Color: color,
Fields: attachmentFields,
}

_, _, err := m.slackApi.PostMessage(
m.slackChannel,
slack.MsgOptionText(text, false),
slack.MsgOptionAttachments(attachment),
)
if err != nil {
panic(err.Error())
}
}

Expand Down Expand Up @@ -93,3 +119,56 @@ func (m Metrics) extractAccounts(accounts []string) string {
return "All accounts in region"
}
}

func ignoreEvents(ignoredEvents []string, event string) bool {
for _, e := range ignoredEvents {
if e == event {
return true
}
}

return false
}

func ignoreResources(ignoredResources []string, resources []healthTypes.AffectedEntity) bool {
size := len(resources)

for _, ignored := range ignoredResources {
for _, resource := range resources {
if *resource.EntityValue == ignored {
size -= 1
}
}
}

if size == 0 {
// all resources are ignored, ignoring entire alert
return true
}

// not all resources are ignored
return false
}

func ignoreResourceEvent(ignoredResourceEvent []string, event HealthEvent) bool {
size := len(event.AffectedResources)

for _, ignored := range ignoredResourceEvent {
tmp := strings.Split(ignored, ":")
ignoredEvent, ignoredResource := tmp[0], tmp[1]

for _, resource := range event.AffectedResources {
if *resource.EntityValue == ignoredResource && *event.Event.EventTypeCode == ignoredEvent {
size -= 1
}
}
}

if size == 0 {
// all resources are ignored, ignoring entire alert
return true
}

// not all resources are ignored
return false
}
31 changes: 19 additions & 12 deletions exporter/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package exporter
import (
"context"
"os"
"sort"
"strings"
"time"
_ "time/tzdata"
Expand Down Expand Up @@ -65,26 +66,32 @@ func (m *Metrics) init(ctx context.Context, c *cli.Context) {

if c.String("regions") != "all-regions" {
m.regions = strings.Split(c.String("regions"), ",")
sort.Strings(m.regions)
}

if len(c.String("ignore-events")) > 0 {
m.ignoreEvents = strings.Split(c.String("ignore-events"), ",")
sort.Strings(m.ignoreEvents)
}

if len(c.String("ignore-resources")) > 0 {
m.ignoreResources = strings.Split(c.String("ignore-resources"), ",")
sort.Strings(m.ignoreResources)
}

if len(c.String("ignore-resource-event")) > 0 {
m.ignoreResourceEvent = strings.Split(c.String("ignore-resource-event"), ",")
sort.Strings(m.ignoreResourceEvent)
}

}

func (m *Metrics) Describe(ch chan<- *prometheus.Desc) {
prometheus.DescribeByCollect(m, ch)
}

func (m *Metrics) Collect(ch chan<- prometheus.Metric) {
var events []HealthEvent
if m.organizationEnabled {
events = m.GetOrgEvents()
} else {
events = m.GetEvents()
}

if len(events) == 0 {
return
}

m.SendSlackNotification(events)
m.GetHealthEvents()
}

func sanitizeLabel(label string) string {
Expand Down
2 changes: 1 addition & 1 deletion exporter/single.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
healthTypes "github.com/aws/aws-sdk-go-v2/service/health/types"
)

func (m *Metrics) GetEvents() []HealthEvent {
func (m *Metrics) GetAccountEvents() []HealthEvent {
ctx := context.TODO()
now := time.Now()
pag := health.NewDescribeEventsPaginator(
Expand Down
4 changes: 4 additions & 0 deletions exporter/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ type Metrics struct {
awsconfig aws.Config
organizationEnabled bool
regions []string

ignoreEvents []string
ignoreResources []string
ignoreResourceEvent []string
}

type HealthEvent struct {
Expand Down
3 changes: 3 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ func main() {
&cli.StringFlag{Name: "slack-token", Usage: "Slack token", EnvVars: []string{"SLACK_TOKEN"}, Required: true},
&cli.StringFlag{Name: "slack-channel", Usage: "Slack channel id", EnvVars: []string{"SLACK_CHANNEL"}, Required: true},
&cli.StringFlag{Name: "assume-role", Usage: "Assume another AWS IAM role", EnvVars: []string{"ASSUME_ROLE"}},
&cli.StringFlag{Name: "ignore-events", Usage: "Comma separated list of events to be ignored on all resources"},
&cli.StringFlag{Name: "ignore-resources", Usage: "Comma separated list of resources to be ignored on all events, format is dependant on resource type (some are ARN others are Name, check AWS docs)"},
&cli.StringFlag{Name: "ignore-resource-event", Usage: "Comma separated list of events to be ignored on a specific resource (format: <event name>:<resource identifier>)"},
}

app := &cli.App{
Expand Down

0 comments on commit 719ba22

Please sign in to comment.