Skip to content
48 changes: 0 additions & 48 deletions cmd/main.go

This file was deleted.

9 changes: 9 additions & 0 deletions configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,29 @@ import (

// Vars
var (
ChannelPrefix = flag.String("slack-channel-prefix", "", "the slack channel prefix")
IncomingWebhookURL = flag.String("slack-incoming-webhook-url", "", "the slack incoming webhook url")
RequestTimeout = flag.Duration("slack-request-timeout", 0, "the duration after which a request is considered as having timed out")
RetryMax = flag.Int("slack-retry-max", 0, "the slack max retry")
RetrySleep = flag.Duration("slack-retry-sleep", 0, "the slack max sleep")
)

// Configuration represents the slack configuration
type Configuration struct {
ChannelPrefix string `toml:"channel_prefix"`
IncomingWebhookURL string `toml:"incoming_webhook_url"`
RequestTimeout time.Duration `toml:"request_timeout"`
RetryMax int `toml:"retry_max"`
RetrySleep time.Duration `toml:"retry_sleep"`
}

// FlagConfig generates a Configuration based on flags
func FlagConfig() Configuration {
return Configuration{
ChannelPrefix: *ChannelPrefix,
IncomingWebhookURL: *IncomingWebhookURL,
RequestTimeout: *RequestTimeout,
RetryMax: *RetryMax,
RetrySleep: *RetrySleep,
}
}
52 changes: 28 additions & 24 deletions http.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"fmt"
"io/ioutil"
"net"
"net/http"
"time"
)
Expand All @@ -13,51 +14,54 @@ var Send = func(req *http.Request, httpClient *http.Client) (*http.Response, err
return httpClient.Do(req)
}

// Send sends a new authorized OHE request
func (o *Slack) Send(hostname string, pattern string, method string, body []byte) (req *http.Request, resp *http.Response, err error) {
// Send sends a new slack
func (s *Slack) Send(hostname string, pattern string, method string, body []byte) (req *http.Request, resp *http.Response, err error) {
// Log
url := hostname + pattern
o.Logger.Debugf("Sending Slack %s request to %s with body %s", method, url, string(body))

// Create request
req, err = http.NewRequest(method, url, bytes.NewReader(body))
if err != nil {
return
}
req.Header.Add("Content-type", "application/json")
defer req.Body.Close()

// Send request
resp, err = Send(req, o.HTTPClient)
resp, err = Send(req, s.HTTPClient)
return
}

// SendWithMaxRetries sends a new authorized OHE request and retries in case of specific conditions
func (o *Slack) SendWithMaxRetries(hostname string, pattern string, method string, body []byte, retryMax int, retrySleep time.Duration) (req *http.Request, resp *http.Response, err error) {
// SendWithMaxRetries sends a new slack and retries in case of specific conditions
func (s *Slack) SendWithMaxRetries(hostname string, pattern string, method string, body []byte) (req *http.Request, resp *http.Response, err error) {
// Loop
for retriesLeft := retryMax; retriesLeft > 0; retriesLeft-- {
// We start at s.RetryMax + 1 so that it runs at least once even if RetryMax == 0
for retriesLeft := s.RetryMax + 1; retriesLeft > 0; retriesLeft-- {
// Send request
req, resp, err = o.Send(hostname, pattern, method, body)
if err != nil {
return
var retry bool
if req, resp, err = s.Send(hostname, pattern, method, body); err != nil {
// If error is temporary, retry
if netError, ok := err.(net.Error); ok && netError.Temporary() {
retry = true
} else {
return
}
}

// Retry if internal server or if too many requests
if resp.StatusCode >= http.StatusInternalServerError || resp.StatusCode == http.StatusTooManyRequests {
if retry || resp.StatusCode >= http.StatusInternalServerError || resp.StatusCode == http.StatusTooManyRequests {
// Get body
b, e := ioutil.ReadAll(resp.Body)
if e != nil {
err = e
return
if resp != nil {
defer resp.Body.Close()
if _, err = ioutil.ReadAll(resp.Body); err != nil {
return
}
}

// Log
o.Logger.Debugf("Status code %d triggered a retry, sleeping %s and retrying... (%d retries left and body %s)", resp.StatusCode, retrySleep, retriesLeft-1, string(b))

// Close response body
resp.Body.Close()

// Sleep
time.Sleep(retrySleep)
if retriesLeft > 1 {
time.Sleep(s.RetrySleep)
}
continue
}

Expand All @@ -66,14 +70,14 @@ func (o *Slack) SendWithMaxRetries(hostname string, pattern string, method strin
}

// Max retries limit reached
err = fmt.Errorf("Max retries %d reached", retryMax)
err = fmt.Errorf("Max retries %d reached for request to %s", s.RetryMax, req.URL)
return
}

// ProcessResponse processes an HTTP response
var ProcessResponse = func(req *http.Request, resp *http.Response) error {
if resp.StatusCode < 200 || resp.StatusCode > 299 {
return fmt.Errorf("Invalid status code %v on %v", resp.StatusCode, req.URL.Path)
return fmt.Errorf("Invalid status code %v on %v", resp.StatusCode, req.URL)
}
return nil
}
19 changes: 10 additions & 9 deletions http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,13 @@ import (
"testing"
"time"

"github.com/asticode/go-slack"
"github.com/rs/xlog"
"github.com/molotovtv/go-slack"
"github.com/stretchr/testify/assert"
)

func TestSendWithMaxRetries(t *testing.T) {
var count int
o := slack.Slack{
Logger: xlog.NopLogger,
}
s := slack.Slack{RetrySleep: time.Nanosecond}
slack.Send = func(req *http.Request, httpClient *http.Client) (*http.Response, error) {
count++
if count == 1 {
Expand All @@ -29,19 +26,23 @@ func TestSendWithMaxRetries(t *testing.T) {
}
return &http.Response{StatusCode: http.StatusBadRequest, ProtoMinor: 4, Body: ioutil.NopCloser(strings.NewReader(""))}, nil
}
_, resp, err := o.SendWithMaxRetries("", "", "", nil, 1, time.Nanosecond)
s.RetryMax = 0
_, resp, err := s.SendWithMaxRetries("", "", "", nil)
assert.Error(t, err)
assert.Equal(t, 1, resp.ProtoMinor)
count = 0
_, resp, err = o.SendWithMaxRetries("", "", "", nil, 2, time.Nanosecond)
s.RetryMax = 1
_, resp, err = s.SendWithMaxRetries("", "", "", nil)
assert.Error(t, err)
assert.Equal(t, 2, resp.ProtoMinor)
count = 0
_, resp, err = o.SendWithMaxRetries("", "", "", nil, 3, time.Nanosecond)
s.RetryMax = 2
_, resp, err = s.SendWithMaxRetries("", "", "", nil)
assert.Error(t, err)
assert.Equal(t, 3, resp.ProtoMinor)
count = 0
_, resp, err = o.SendWithMaxRetries("", "", "", nil, 4, time.Nanosecond)
s.RetryMax = 3
_, resp, err = s.SendWithMaxRetries("", "", "", nil)
assert.NoError(t, err)
assert.Equal(t, 4, resp.ProtoMinor)
}
Expand Down
18 changes: 8 additions & 10 deletions message.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,20 @@ package slack

import (
"encoding/json"
"fmt"
"net/http"
"time"
)

// Message represents a message
//https://api.slack.com/messaging/sending
//https://api.slack.com/reference/messaging/payload
type Message struct {
Attachments []Attachment `json:"attachments,omitempty"`
Channel string `json:"channel"`
Markdown bool `json:"mrkdwn,omitempty"`
Text string `json:"text,omitempty"`
Username string `json:"username,omitempty"`
IconeURL string `json:"icon_url,omitempty"`
IconeEmoji string `json:"icon_emoji,omitempty"`
}

// Attachment represents an attachments
Expand Down Expand Up @@ -45,23 +47,19 @@ type Field struct {

// Slack sends a message to Slack
func (s *Slack) Slack(m Message) (err error) {
// Log
l := fmt.Sprintf("Slacking message to %s", m.Channel)
s.Logger.Debugf("[Start] %s", l)
defer func(now time.Time) {
s.Logger.Debugf("[End] %s in %s", l, time.Since(now))
}(time.Now())

// TODO Make sure texts are HTML encoded

// Add channel prefix
m.Channel = s.ChannelPrefix + m.Channel

// Encode message
var b []byte
if b, err = json.Marshal(m); err != nil {
return
}

// Send request
req, resp, err := s.SendWithMaxRetries(s.IncomingWebhookURL, "", http.MethodPost, b, RetryMax, RetrySleep)
req, resp, err := s.SendWithMaxRetries(s.IncomingWebhookURL, "", http.MethodPost, b)
if err != nil {
return
}
Expand Down
12 changes: 6 additions & 6 deletions slack.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,34 @@ package slack
import (
"net/http"
"time"

"github.com/rs/xlog"
)

// Constants
const (
ColorDanger = "danger"
ColorGood = "good"
ColorWarning = "warning"
RetryMax = 5
RetrySleep = time.Minute
)

// Slack represents a Slack communicator
type Slack struct {
ChannelPrefix string
HTTPClient *http.Client
IncomingWebhookURL string
Logger xlog.Logger
RetryMax int
RetrySleep time.Duration
}

// New creates a new Slack communicator
func New(c Configuration) *Slack {
o := &Slack{
ChannelPrefix: c.ChannelPrefix,
HTTPClient: &http.Client{
Timeout: c.RequestTimeout,
},
IncomingWebhookURL: c.IncomingWebhookURL,
Logger: xlog.NopLogger,
RetryMax: c.RetryMax,
RetrySleep: c.RetrySleep,
}
if c.RequestTimeout == 0 {
o.HTTPClient.Timeout = time.Duration(10) * time.Second
Expand Down