Skip to content
This repository has been archived by the owner on Jul 27, 2023. It is now read-only.

Commit

Permalink
Daemon: Add support for slack target
Browse files Browse the repository at this point in the history
  • Loading branch information
pranj committed Feb 13, 2017
1 parent 77cd22e commit 2be0e8e
Show file tree
Hide file tree
Showing 5 changed files with 248 additions and 4 deletions.
4 changes: 4 additions & 0 deletions target/emailtype.go
Expand Up @@ -18,6 +18,10 @@ import (

type emailType struct{}

func init() {
registerTargetType(emailType{})
}

func (_ emailType) ID() db.TargetType {
return 1
}
Expand Down
24 changes: 24 additions & 0 deletions target/slack.go
@@ -0,0 +1,24 @@
package target

import (
"github.com/jmoiron/sqlx/types"
"github.com/juju/errors"
)

type Slack struct {
Channel string
}

func newSlack(configJSON types.JSONText) (Target, error) {
var config SlackDBModel
err := configJSON.Unmarshal(&config)
if err != nil {
return nil, errors.Maskf(err, "deserialize target config")
}

return &Slack{Channel: config.Channel}, nil
}

func (Slack) Type() Type {
return slackType{}
}
126 changes: 126 additions & 0 deletions target/slacknotifier.go
@@ -0,0 +1,126 @@
package target

import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"

"github.com/juju/errors"
"github.com/yext/revere/state"
)

const timeFormat = "Mon Jan 2 2006 15:04:05 MST"

var (
stateColors = map[state.State]string{
// Green
state.Normal: "good",
// Yellow
state.Warning: "warning",
// Red
state.Error: "danger",
// Black
state.Critical: "#000000",
// Grey
state.Unknown: "#808080",
}
)

type slackNotifier struct {
alert *Alert
name string
url string
}

type payload struct {
Username string `json:"username"`
Channel string `json:"channel,omitempty"`
Attachments []attachment `json:"attachments"`
}

type attachment struct {
Title string `json:"title"`
TitleLink string `json:"title_link"`
Fallback string `json:"fallback"`
Color string `json:"color"`
Text string `json:"text"`
Timestamp int64 `json:"ts"`
}

func (s slackNotifier) sendAll(channels map[string]struct{}) error {
var (
failedChannelNames []string
err error
)

for channel, _ := range channels {
err = s.send(channel)
if err != nil {
failedChannelNames = append(failedChannelNames, channel)
}
}

if len(failedChannelNames) > 0 {
return errors.Maskf(err, "sending slack notifications to %v", failedChannelNames)
}
return nil
}

func (s slackNotifier) send(channel string) error {
message, err := s.formatMessage(channel)
if err != nil {
return errors.Maskf(err, "formatting slack message")
}

resp, err := http.Post(s.url, "application/json", message)
if err != nil {
return errors.Maskf(err, "sending slack notification to: %s", channel)
}
if resp.StatusCode != http.StatusOK {
return errors.Errorf(
"not-OK HTTP status code: %d, when sending slack notification to: %s",
resp.StatusCode,
channel)
}
return nil
}

func (s slackNotifier) formatMessage(channel string) (io.Reader, error) {
var text string
if s.alert.OldState != s.alert.NewState {
text = fmt.Sprintf("State change: %s->%s", s.alert.OldState, s.alert.NewState)
} else {
text = fmt.Sprintf("Has been %s since: %s",
s.alert.NewState, s.alert.EnteredState.UTC().Format(timeFormat))
}

if s.alert.NewState != state.Normal {
text = fmt.Sprintf("%s\nWas last Normal at: %s",
text, s.alert.LastNormal.UTC().Format(timeFormat))
}

payload := payload{
Username: s.name,
Channel: channel,
Attachments: []attachment{
{
Title: fmt.Sprintf("%s/%s", s.alert.MonitorName, s.alert.SubprobeName),
TitleLink: fmt.Sprintf("http://revere.khan/monitors/%d/subprobes/%d",
s.alert.MonitorID, s.alert.SubprobeID),
Fallback: fmt.Sprintf("%s/%s entered state: %s",
s.alert.MonitorName, s.alert.SubprobeName, s.alert.NewState),
Color: stateColors[s.alert.NewState],
Text: text,
Timestamp: s.alert.Recorded.Unix(),
},
},
}

buf, err := json.Marshal(payload)
if err != nil {
return nil, err
}
return bytes.NewBuffer(buf), nil
}
77 changes: 77 additions & 0 deletions target/slacktype.go
@@ -0,0 +1,77 @@
package target

import (
"github.com/jmoiron/sqlx/types"
"github.com/juju/errors"

"github.com/yext/revere/db"
"github.com/yext/revere/setting"
)

type slackType struct{}

func init() {
registerTargetType(slackType{})
}

func (slackType) ID() db.TargetType {
return 2
}

func (slackType) New(config types.JSONText) (Target, error) {
return newSlack(config)
}

func (slackType) Alert(
Db *db.DB, a *Alert, toAlert map[db.TriggerID]Target, inactive []Target) []ErrorAndTriggerIDs {
triggerIDs := make([]db.TriggerID, 0, len(toAlert))
for id := range toAlert {
triggerIDs = append(triggerIDs, id)
}

channels := make(map[string]struct{})
for _, target := range toAlert {
target := target.(*Slack)
channels[target.Channel] = struct{}{}
}

slackSetting := setting.SlackSetting{}
dbSettings, err := Db.LoadSettingsOfType(slackSetting.Type().Id())
if err != nil || len(dbSettings) == 0 {
return []ErrorAndTriggerIDs{{
Err: errors.Maskf(err, "getting settings from db"),
IDs: triggerIDs,
}}
}

settingsFromDB, err := setting.LoadFromDB(slackSetting.Type().Id(), dbSettings[0].Setting)
if err != nil {
return []ErrorAndTriggerIDs{{
Err: errors.Maskf(err, "unmarshalling db settings"),
IDs: triggerIDs,
}}
}

slackSettings, found := settingsFromDB.(*setting.SlackSetting)
if !found {
return []ErrorAndTriggerIDs{{
Err: errors.Maskf(err, "extracting slack settings"),
IDs: triggerIDs,
}}
}

notifier := slackNotifier{
alert: a,
name: slackSettings.BotName,
url: slackSettings.WebhookURL,
}
err = notifier.sendAll(channels)
if err != nil {
return []ErrorAndTriggerIDs{{
Err: errors.Trace(err),
IDs: triggerIDs,
}}
}

return nil
}
21 changes: 17 additions & 4 deletions target/target.go
Expand Up @@ -2,23 +2,36 @@
package target

import (
"fmt"

"github.com/jmoiron/sqlx/types"
"github.com/juju/errors"

"github.com/yext/revere/db"
)

var (
daemonTargetTypes = make(map[db.TargetType]Type)
)

// Target defines a common abstraction for individual targets.
type Target interface {
Type() Type
}

// New makes a Target of the given type and settings.
func New(typeID db.TargetType, config types.JSONText) (Target, error) {
// TODO(eefi): Implement Type dictionary system.
if typeID != 1 {
return nil, errors.Errorf("unknown target type %d", typeID)
if targetType, found := daemonTargetTypes[typeID]; found {
return targetType.New(config)
}
return nil, errors.Errorf("unknown target type %d", typeID)
}

return emailType{}.New(config)
// registerTargetType registers a target type onto a type dictionary
func registerTargetType(t Type) {
if _, exists := daemonTargetTypes[t.ID()]; !exists {
daemonTargetTypes[t.ID()] = t
} else {
panic(fmt.Sprintf("A target type with id %d already exists", t.ID()))
}
}

0 comments on commit 2be0e8e

Please sign in to comment.