Skip to content

Commit

Permalink
Refactor body parsing
Browse files Browse the repository at this point in the history
  • Loading branch information
shamil committed Sep 15, 2019
1 parent f2a02e6 commit 424f09e
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 84 deletions.
43 changes: 28 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

An HTTP server which used to handle webhooks triggered by [OpenDistro for Elasticsearch Alerting](https://opendistro.github.io/for-elasticsearch-docs/docs/alerting)

> Notice, the readme is for `0.2.x` version
## Why?

As for time of writing `destination` options that `ODFE` provides are limited.
Expand Down Expand Up @@ -46,26 +48,37 @@ Download latest version for your platform from [releases](https://github.com/bri

1. Go to `Alerting` > `Destinations`
2. Create the destination with type `Custom webhook`
3. Chose `Define endpoint by custom attributes URL`
3. Choose `Define endpoint by URL`
- For `slack` set the url to have path with `/slack`, like `http://odfe-server:8080/slack`
- For `email` set the url to have path with `/email`, like `http://odfe-server:8080/email`

### Sending Email from triggers

Fill in `Type`, `Host` and `Port` according to how and where you installed `odfe-alerts-handler`.
1. Select destination which was created with the `/email` path
2. The `Message` body look like below:

### Configuring for Email
```yaml
to: ['[email protected]']
subject: ['Optional subject param']
---
This is the body of the message
Here you can use the templeting as usual...
```

1. Set `Path` to `email`
2. Set `Query parameters` as follows:
- Key: `addresses`
- Value: comma separated list of emails to send
`subject` is optional, if not provided the default one, see [#usage](usage).

You can also override the default subject.
If the first line of the alert message contains `Subject:`, that line will be used as a subject for the email.
### Sending Slack from triggers

### Configuring for Slack
1. Select destination which was created with the `/slack` path
2. The `Message` body look like below:

1. Set `Path` to `slack`
2. Set `Query parameters` as follows:
- Key: `channels` or `users`
- Value: comma separated list of user **emails** or list of channels
```yaml
channels: ['#alerts']
users: ['[email protected]']
---
This is the body of the message
Here you can use the templeting as usual...
```

You can have both `channels` and `users` keys if you desire to send to both.
Optionally, for `channels` you can omit the leading `#`.
Expand All @@ -74,7 +87,7 @@ Optionally, for `channels` you can omit the leading `#`.

```shell
RELEASE_TITLE="First release"
RELEASE_VERSION=0.1.0
RELEASE_VERSION=0.2.0

git tag -a v${RELEASE_VERSION} -m "${RELEASE_TITLE}"
git push --tags
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ require (
github.com/nlopes/slack v0.6.0
golang.org/x/crypto v0.0.0-20190829043050-9756ffdc2472 // indirect
gopkg.in/alecthomas/kingpin.v2 v2.2.6
gopkg.in/yaml.v2 v2.2.2
)
35 changes: 30 additions & 5 deletions handlers/common.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,34 @@
package handlers

import "strings"
import (
"fmt"
"io"
"io/ioutil"
"regexp"

func fields(s string, sep rune) []string {
return strings.FieldsFunc(s, func(c rune) bool {
return c == sep
})
"gopkg.in/yaml.v2"
)

var bodySep = regexp.MustCompile("(?:^|\\s*\n)---\\s*")

func parseBody(requestBody io.ReadCloser, target interface{}) (string, error) {
body, err := ioutil.ReadAll(requestBody)

if err != nil {
return "", fmt.Errorf("failed to read body, %v", err)
}

docs := bodySep.Split(string(body), 2)

if len(docs) != 2 {
return "", fmt.Errorf("cannot split body, got %d elements after split, but 2 elements required", len(docs))
}

params, data := []byte(docs[0]), docs[1]

if err := yaml.Unmarshal(params, target); err != nil {
return "", fmt.Errorf("cannot unmarshal params, %v", err)
}

return data, nil
}
59 changes: 19 additions & 40 deletions handlers/email.go
Original file line number Diff line number Diff line change
@@ -1,23 +1,17 @@
package handlers

import (
"bufio"
"bytes"
"fmt"
"io/ioutil"
"net"
"net/http"
"net/smtp"
"regexp"
"strconv"

emailClient "github.com/jordan-wright/email"
"github.com/labstack/echo/v4"
"github.com/labstack/gommon/log"
)

var subjectRe = regexp.MustCompile(`(?i)^(\s+)?subject:(\s+)?`)

// Email used to configure common params for sending email
type Email struct {
Host string
Expand All @@ -30,31 +24,18 @@ type Email struct {

// email used by the handler to set params per incoming request
type email struct {
Email
*Email

subject string
to []string
Subject string
To []string
data []byte
}

func (e *email) prepareSubject() {
scanner := bufio.NewScanner(bytes.NewReader(e.data))
scanner.Scan()

if subjectRe.Match(scanner.Bytes()) {
e.subject = subjectRe.ReplaceAllString(scanner.Text(), "")
e.data = e.data[len(scanner.Bytes()):]
return
}

e.subject = e.DefaultSubject
}

func (e *email) send() error {
client := &emailClient.Email{
To: e.to,
To: e.To,
From: e.From,
Subject: e.subject,
Subject: e.Subject,
Text: e.data,
}

Expand All @@ -68,40 +49,38 @@ func (e *email) send() error {

// EchoHandler sends email per each incoming http request
func (e Email) EchoHandler(c echo.Context) error {
addresses := fields(c.QueryParam("addresses"), ',')

if len(addresses) == 0 {
response := "email was not sent, no addresses param provided"

log.Error(response)
return echo.NewHTTPError(http.StatusBadRequest, response)
emailer := email{
Email: &e,
Subject: e.DefaultSubject,
}

defer c.Request().Body.Close()
requestBody, err := ioutil.ReadAll(c.Request().Body)
data, err := parseBody(c.Request().Body, &emailer)

if err != nil {
response := fmt.Sprintf("email was not sent, failed to read body, %v", err)
response := fmt.Sprintf("email was not sent, %v", err)

log.Error(response)
return echo.NewHTTPError(http.StatusInternalServerError, response)
}

emailer := email{
Email: e,
data: requestBody,
to: addresses,
if len(emailer.To) == 0 {
response := "email was not sent, 'To' param wasn't provided"

log.Error(response)
return echo.NewHTTPError(http.StatusBadRequest, response)
}

emailer.prepareSubject()
emailer.data = []byte(data)

if err := emailer.send(); err != nil {
response := fmt.Sprintf("email was not sent, to: %v, subject: %s, %v", addresses, emailer.subject, err)
response := fmt.Sprintf("email was not sent, to: %v, subject: %s, %v", emailer.To, emailer.Subject, err)

log.Error(response)
return echo.NewHTTPError(http.StatusInternalServerError, response)
}

response := fmt.Sprintf("email successfuly sent, to: %v, subject: %s", addresses, emailer.subject)
response := fmt.Sprintf("email successfuly sent, to: %v, subject: %s", emailer.To, emailer.Subject)
log.Info(response)
return echo.NewHTTPError(http.StatusOK, response)
}
45 changes: 21 additions & 24 deletions handlers/slack.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package handlers

import (
"fmt"
"io/ioutil"
"net/http"
"strings"

Expand All @@ -20,10 +19,10 @@ type Slack struct {

// slack used by the handler to set params per incoming request
type slack struct {
Slack
*Slack `yaml:"-"`

channels []string
users []string
Channels []string
Users []string
text string
}

Expand All @@ -38,7 +37,7 @@ func (s *slack) getClient() *slackAPI.Client {
func (s slack) postToChannels() error {
var result error

for _, channel := range s.channels {
for _, channel := range s.Channels {
if !strings.HasPrefix(channel, "#") {
channel = "#" + channel
}
Expand All @@ -54,7 +53,7 @@ func (s slack) postToChannels() error {
func (s slack) postToUsers() error {
var result error

for _, user := range s.users {
for _, user := range s.Users {

user, err := s.getClient().GetUserByEmail(user)
if err != nil {
Expand All @@ -80,13 +79,13 @@ func (s slack) postToUsers() error {
func (s slack) post() error {
var result error

if len(s.channels) > 0 {
if len(s.Channels) > 0 {
if err := s.postToChannels(); err != nil {
result = multierror.Append(result, err)
}
}

if len(s.users) > 0 {
if len(s.Users) > 0 {
if err := s.postToUsers(); err != nil {
result = multierror.Append(result, err)
}
Expand All @@ -104,39 +103,37 @@ func (s Slack) EchoHandler(c echo.Context) error {
return echo.NewHTTPError(http.StatusUnprocessableEntity, response)
}

channels, users := fields(c.QueryParam("channels"), ','), fields(c.QueryParam("users"), ',')

if len(channels) == 0 && len(users) == 0 {
response := "slack message was not sent, no channels or users params provided"

log.Error(response)
return echo.NewHTTPError(http.StatusBadRequest, response)
slacker := slack{
Slack: &s,
}

defer c.Request().Body.Close()
requestBody, err := ioutil.ReadAll(c.Request().Body)
text, err := parseBody(c.Request().Body, &slacker)

if err != nil {
response := fmt.Sprintf("slack message was not sent, failed to read body, %v", err)
response := fmt.Sprintf("slack message was not sent, %v", err)

log.Error(response)
return echo.NewHTTPError(http.StatusInternalServerError, response)
}

slacker := slack{
Slack: s,
channels: channels,
users: users,
text: string(requestBody),
if len(slacker.Channels) == 0 && len(slacker.Users) == 0 {
response := "slack message was not sent, no channels or users params provided"

log.Error(response)
return echo.NewHTTPError(http.StatusBadRequest, response)
}

slacker.text = text

if err := slacker.post(); err != nil {
response := fmt.Sprintf("slack message was not sent, channels: %v, users: %v, %v", channels, users, err)
response := fmt.Sprintf("slack message was not sent, channels: %v, users: %v, %v", slacker.Channels, slacker.Users, err)

log.Error(response)
return echo.NewHTTPError(http.StatusInternalServerError, response)
}

response := fmt.Sprintf("slack message successfuly sent, channels: %v, users: %v", channels, users)
response := fmt.Sprintf("slack message successfuly sent, channels: %v, users: %v", slacker.Channels, slacker.Users)
log.Info(response)
return echo.NewHTTPError(http.StatusOK, response)
}

0 comments on commit 424f09e

Please sign in to comment.