diff --git a/bench_test.go b/bench_test.go index 1dd03f0..3f22c0d 100644 --- a/bench_test.go +++ b/bench_test.go @@ -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() diff --git a/notifier.go b/notifier.go index 3fac580..768c78a 100644 --- a/notifier.go +++ b/notifier.go @@ -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 @@ -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") @@ -190,6 +197,8 @@ type Notifier struct { rateLimitReset uint32 // atomic _closed uint32 // atomic + + remoteConfig *remoteConfig } func NewNotifierWithOptions(opt *NotifierOptions) *Notifier { @@ -202,6 +211,8 @@ func NewNotifierWithOptions(opt *NotifierOptions) *Notifier { Routes: newRoutes(opt), Queries: newQueryStats(opt), Queues: newQueueStats(opt), + + remoteConfig: newRemoteConfig(opt), } n.AddFilter(httpUnsolicitedResponseFilter) @@ -216,6 +227,8 @@ func NewNotifierWithOptions(opt *NotifierOptions) *Notifier { n.AddFilter(NewBlocklistKeysFilter(opt.KeysBlocklist...)) } + n.remoteConfig.Poll() + return n } @@ -411,6 +424,7 @@ func (n *Notifier) Flush() { } func (n *Notifier) Close() error { + n.remoteConfig.StopPolling() return n.CloseTimeout(waitTimeout) } diff --git a/notifier_test.go b/notifier_test.go index d2855a2..f63925d 100644 --- a/notifier_test.go +++ b/notifier_test.go @@ -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 @@ -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, } }) @@ -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) @@ -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, } }) @@ -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, }) }) @@ -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, }) }) @@ -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, }) }) @@ -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, } }) diff --git a/race_test.go b/race_test.go index 39c8643..0656ad7 100644 --- a/race_test.go +++ b/race_test.go @@ -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, }) }) diff --git a/remote_config.go b/remote_config.go new file mode 100644 index 0000000..a32a2cd --- /dev/null +++ b/remote_config.go @@ -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 +} diff --git a/remote_config_test.go b/remote_config_test.go new file mode 100644 index 0000000..15ba60e --- /dev/null +++ b/remote_config_test.go @@ -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()) + }) + }) + }) +})