Skip to content

Commit

Permalink
Fetch remote configuration every 10 minutes
Browse files Browse the repository at this point in the history
Gobrake needs to be able to fetch remote configuration at certain
intervals. This commit prepares gobrake for the change. A newly created notifier
will start polling S3 for the config immediately. The config values are not used
at the moment, we just make reads and discard the config to keep the change as
simple as possible.
  • Loading branch information
kyrylo committed Aug 12, 2020
1 parent 1995d61 commit cbfed09
Show file tree
Hide file tree
Showing 6 changed files with 233 additions and 27 deletions.
8 changes: 5 additions & 3 deletions bench_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@ func BenchmarkSendNotice(b *testing.B) {
}
}
server := httptest.NewServer(http.HandlerFunc(handler))
configServer := newConfigServer()

notifier := gobrake.NewNotifierWithOptions(&gobrake.NotifierOptions{
ProjectId: 1,
ProjectKey: "key",
Host: server.URL,
ProjectId: 1,
ProjectKey: "key",
Host: server.URL,
RemoteConfigHost: configServer.URL,
})

b.ResetTimer()
Expand Down
14 changes: 14 additions & 0 deletions notifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ type NotifierOptions struct {
// Airbrake host name for sending APM data.
APMHost string

// The host name where the remote config is located.
RemoteConfigHost string

// Environment such as production or development.
Environment string

Expand Down Expand Up @@ -107,6 +110,10 @@ func (opt *NotifierOptions) init() {
opt.APMHost = opt.Host
}

if opt.RemoteConfigHost == "" {
opt.RemoteConfigHost = "https://v1-staging-notifier-config.s3.amazonaws.com"
}

if opt.Revision == "" {
// https://devcenter.heroku.com/changelog-items/630
opt.Revision = os.Getenv("SOURCE_VERSION")
Expand Down Expand Up @@ -190,6 +197,8 @@ type Notifier struct {

rateLimitReset uint32 // atomic
_closed uint32 // atomic

remoteConfig *remoteConfig
}

func NewNotifierWithOptions(opt *NotifierOptions) *Notifier {
Expand All @@ -202,6 +211,8 @@ func NewNotifierWithOptions(opt *NotifierOptions) *Notifier {
Routes: newRoutes(opt),
Queries: newQueryStats(opt),
Queues: newQueueStats(opt),

remoteConfig: newRemoteConfig(opt),
}

n.AddFilter(httpUnsolicitedResponseFilter)
Expand All @@ -216,6 +227,8 @@ func NewNotifierWithOptions(opt *NotifierOptions) *Notifier {
n.AddFilter(NewBlocklistKeysFilter(opt.KeysBlocklist...))
}

n.remoteConfig.Poll()

return n
}

Expand Down Expand Up @@ -411,6 +424,7 @@ func (n *Notifier) Flush() {
}

func (n *Notifier) Close() error {
n.remoteConfig.StopPolling()
return n.CloseTimeout(waitTimeout)
}

Expand Down
65 changes: 44 additions & 21 deletions notifier_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@ func TestGobrake(t *testing.T) {
RunSpecs(t, "gobrake")
}

func newConfigServer() *httptest.Server {
handler := func(w http.ResponseWriter, req *http.Request) {
w.WriteHeader(http.StatusOK)
_, err := w.Write([]byte(`{}`))
Expect(err).To(BeNil())
}
return httptest.NewServer(http.HandlerFunc(handler))
}

var _ = Describe("Notifier", func() {
var notifier *gobrake.Notifier
var sentNotice *gobrake.Notice
Expand Down Expand Up @@ -58,11 +67,13 @@ var _ = Describe("Notifier", func() {
Expect(err).To(BeNil())
}
server := httptest.NewServer(http.HandlerFunc(handler))
configServer := newConfigServer()

opt = &gobrake.NotifierOptions{
ProjectId: 1,
ProjectKey: "key",
Host: server.URL,
ProjectId: 1,
ProjectKey: "key",
Host: server.URL,
RemoteConfigHost: configServer.URL,
}
})

Expand Down Expand Up @@ -351,10 +362,12 @@ var _ = Describe("Notifier", func() {
l := log.New(buf, "", 0)
gobrake.SetLogger(l)

configServer := newConfigServer()
n := gobrake.NewNotifierWithOptions(
&gobrake.NotifierOptions{
ProjectId: 1,
ProjectKey: "broken-key",
ProjectId: 1,
ProjectKey: "broken-key",
RemoteConfigHost: configServer.URL,
},
)
n.Notify(errors.New("oops"), nil)
Expand Down Expand Up @@ -417,12 +430,14 @@ var _ = Describe("Deprecated filter keys option", func() {
Expect(err).To(BeNil())
}
server := httptest.NewServer(http.HandlerFunc(handler))
configServer := newConfigServer()

opt = &gobrake.NotifierOptions{
ProjectId: 1,
ProjectKey: "key",
Host: server.URL,
KeysBlacklist: deprecatedKeysOption,
ProjectId: 1,
ProjectKey: "key",
Host: server.URL,
RemoteConfigHost: configServer.URL,
KeysBlacklist: deprecatedKeysOption,
}
})

Expand Down Expand Up @@ -477,11 +492,13 @@ var _ = Describe("rate limiting", func() {
w.WriteHeader(429)
}
server := httptest.NewServer(http.HandlerFunc(handler))
configServer := newConfigServer()

notifier = gobrake.NewNotifierWithOptions(&gobrake.NotifierOptions{
ProjectId: 1,
ProjectKey: "key",
Host: server.URL,
ProjectId: 1,
ProjectKey: "key",
Host: server.URL,
RemoteConfigHost: configServer.URL,
})
})

Expand Down Expand Up @@ -509,11 +526,13 @@ var _ = Describe("Notice exceeds 64KB", func() {
w.WriteHeader(http.StatusCreated)
}
server := httptest.NewServer(http.HandlerFunc(handler))
configServer := newConfigServer()

notifier = gobrake.NewNotifierWithOptions(&gobrake.NotifierOptions{
ProjectId: 1,
ProjectKey: "key",
Host: server.URL,
ProjectId: 1,
ProjectKey: "key",
Host: server.URL,
RemoteConfigHost: configServer.URL,
})
})

Expand Down Expand Up @@ -542,11 +561,13 @@ var _ = Describe("server returns HTTP 400 error message", func() {
Expect(err).To(BeNil())
}
server := httptest.NewServer(http.HandlerFunc(handler))
configServer := newConfigServer()

notifier = gobrake.NewNotifierWithOptions(&gobrake.NotifierOptions{
ProjectId: 1,
ProjectKey: "key",
Host: server.URL,
ProjectId: 1,
ProjectKey: "key",
Host: server.URL,
RemoteConfigHost: configServer.URL,
})
})

Expand Down Expand Up @@ -593,11 +614,13 @@ var _ = Describe("Notifier request filter", func() {
w.WriteHeader(http.StatusCreated)
}
server := httptest.NewServer(http.HandlerFunc(handler))
configServer := newConfigServer()

opt = &gobrake.NotifierOptions{
ProjectId: 1,
ProjectKey: "key",
Host: server.URL,
ProjectId: 1,
ProjectKey: "key",
Host: server.URL,
RemoteConfigHost: configServer.URL,
}

})
Expand Down
8 changes: 5 additions & 3 deletions race_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@ var _ = Describe("Notifier", func() {
}
}
server := httptest.NewServer(http.HandlerFunc(handler))
configServer := newConfigServer()

notifier = gobrake.NewNotifierWithOptions(&gobrake.NotifierOptions{
ProjectId: 1,
ProjectKey: "key",
Host: server.URL,
ProjectId: 1,
ProjectKey: "key",
Host: server.URL,
RemoteConfigHost: configServer.URL,
})
})

Expand Down
67 changes: 67 additions & 0 deletions remote_config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package gobrake

import (
"errors"
"fmt"
"io/ioutil"
"net/http"
"time"
)

type remoteConfig struct {
opt *NotifierOptions
ticker *time.Ticker
}

func newRemoteConfig(opt *NotifierOptions) *remoteConfig {
return &remoteConfig{
opt: opt,
}
}

func (rc *remoteConfig) Poll() {
err := rc.fetchConfig()
if err != nil {
logger.Printf(fmt.Sprintf("fetchConfig failed: %s", err))
}

rc.ticker = time.NewTicker(10 * time.Minute)
go func() {
for {
<-rc.ticker.C
err := rc.fetchConfig()
if err != nil {
logger.Printf(fmt.Sprintf("fetchConfig failed: %s", err))
continue
}
}
}()
}

func (rc *remoteConfig) StopPolling() {
rc.ticker.Stop()
}

func (rc *remoteConfig) fetchConfig() error {
url := fmt.Sprintf("%s/2020-06-18/config/%d/config.json",
rc.opt.RemoteConfigHost, rc.opt.ProjectId)
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()

body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}

switch resp.StatusCode {
case http.StatusForbidden, http.StatusNotFound:
return errors.New(string(body))
case http.StatusOK:
return nil
}

return nil
}
98 changes: 98 additions & 0 deletions remote_config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package gobrake

import (
"bytes"
"log"
"net/http"
"net/http/httptest"

. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)

var _ = Describe("newRemoteConfig", func() {
var rc *remoteConfig
var opt *NotifierOptions
var origLogger *log.Logger
var logBuf *bytes.Buffer

BeforeEach(func() {
opt = &NotifierOptions{
ProjectId: 1,
ProjectKey: "key",
}

origLogger = GetLogger()
logBuf = new(bytes.Buffer)
SetLogger(log.New(logBuf, "", 0))
})

JustBeforeEach(func() {
rc = newRemoteConfig(opt)
})

AfterEach(func() {
SetLogger(origLogger)
rc.StopPolling()
})

Describe("Poll", func() {
Context("when the server returns 404", func() {
BeforeEach(func() {
handler := func(w http.ResponseWriter, req *http.Request) {
w.WriteHeader(http.StatusNotFound)
_, err := w.Write([]byte("not found"))
Expect(err).To(BeNil())
}
server := httptest.NewServer(http.HandlerFunc(handler))

opt.RemoteConfigHost = server.URL
})

It("logs the error", func() {
rc.Poll()
Expect(logBuf.String()).To(
ContainSubstring("fetchConfig failed: not found"),
)
})
})

Context("when the server returns 403", func() {
BeforeEach(func() {
handler := func(w http.ResponseWriter, req *http.Request) {
w.WriteHeader(http.StatusForbidden)
_, err := w.Write([]byte("forbidden"))
Expect(err).To(BeNil())
}
server := httptest.NewServer(http.HandlerFunc(handler))

opt.RemoteConfigHost = server.URL
})

It("logs the error", func() {
rc.Poll()
Expect(logBuf.String()).To(
ContainSubstring("fetchConfig failed: forbidden"),
)
})
})

Context("when the server returns 200", func() {
BeforeEach(func() {
handler := func(w http.ResponseWriter, req *http.Request) {
w.WriteHeader(http.StatusOK)
_, err := w.Write([]byte("{}"))
Expect(err).To(BeNil())
}
server := httptest.NewServer(http.HandlerFunc(handler))

opt.RemoteConfigHost = server.URL
})

It("doesn't log any errors", func() {
rc.Poll()
Expect(logBuf.String()).To(BeEmpty())
})
})
})
})

0 comments on commit cbfed09

Please sign in to comment.