diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index ec89193..1f7eb6c 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -50,6 +50,9 @@ jobs: [github] client-id = "${{ secrets.GH_CLIENT_ID }}" client-secret = "${{ secrets.GH_CLIENT_SECRET }}" + + [rollbar] + token = "${{ secrets.ROLLBAR_TOKEN }}" EOF - name: Copy file to server diff --git a/cmd/wtfd/main.go b/cmd/wtfd/main.go index bfda994..c2852d2 100644 --- a/cmd/wtfd/main.go +++ b/cmd/wtfd/main.go @@ -18,6 +18,7 @@ import ( "github.com/benbjohnson/wtf/inmem" "github.com/benbjohnson/wtf/sqlite" "github.com/pelletier/go-toml" + "github.com/rollbar/rollbar-go" ) // Build version, injected during build. @@ -146,6 +147,17 @@ func (m *Main) ParseFlags(ctx context.Context, args []string) error { // Run executes the program. The configuration should already be set up before // calling this function. func (m *Main) Run(ctx context.Context) (err error) { + // Initialize error tracking. + if m.Config.Rollbar.Token != "" { + rollbar.SetToken(m.Config.Rollbar.Token) + rollbar.SetEnvironment("production") + rollbar.SetCodeVersion(version) + rollbar.SetServerRoot("github.com/benbjohnson/wtf") + wtf.ReportError = rollbarReportError + wtf.ReportPanic = rollbarReportPanic + log.Printf("rollbar error tracking enabled") + } + // Initialize event service for real-time events. // We are using an in-memory implementation but this could be changed to // a more robust service if we expanded out to multiple nodes. @@ -240,6 +252,10 @@ type Config struct { ClientID string `toml:"client-id"` ClientSecret string `toml:"client-secret"` } `toml:"github"` + + Rollbar struct { + Token string `toml:"token"` + } `toml:"rollbar"` } // DefaultConfig returns a new instance of Config with defaults set. @@ -289,3 +305,24 @@ func expandDSN(dsn string) (string, error) { } return expand(dsn) } + +// rollbarReportError reports internal errors to rollbar. +func rollbarReportError(ctx context.Context, err error, args ...interface{}) { + if wtf.ErrorCode(err) != wtf.EINTERNAL { + return + } + + // Set user information for error, if available. + if u := wtf.UserFromContext(ctx); u != nil { + rollbar.SetPerson(fmt.Sprint(u.ID), u.Name, u.Email) + } else { + rollbar.ClearPerson() + } + + rollbar.Error(append([]interface{}{err}, args)...) +} + +// rollbarReportPanic reports panics to rollbar. +func rollbarReportPanic(err interface{}) { + rollbar.LogPanic(err, true) +} diff --git a/go.mod b/go.mod index 84e482e..a6c14ec 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( github.com/gorilla/websocket v1.4.2 github.com/mattn/go-sqlite3 v1.14.4 github.com/pelletier/go-toml v1.8.1 + github.com/rollbar/rollbar-go v1.2.0 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58 ) diff --git a/go.sum b/go.sum index 7635877..d16450d 100644 --- a/go.sum +++ b/go.sum @@ -148,6 +148,8 @@ github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrap github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rollbar/rollbar-go v1.2.0 h1:CUanFtVu0sa3QZ/fBlgevdGQGLWaE3D4HxoVSQohDfo= +github.com/rollbar/rollbar-go v1.2.0/go.mod h1:czC86b8U4xdUH7W2C6gomi2jutLm8qK0OtrF5WMvpcc= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= diff --git a/http/http.go b/http/http.go index 1b14b9d..997e369 100644 --- a/http/http.go +++ b/http/http.go @@ -69,8 +69,9 @@ func Error(w http.ResponseWriter, r *http.Request, err error) { // Extract error code & message. code, message := wtf.ErrorCode(err), wtf.ErrorMessage(err) - // Log internal errors. + // Log & report internal errors. if code == wtf.EINTERNAL { + wtf.ReportError(r.Context(), err, r) LogError(r, err) } diff --git a/http/server.go b/http/server.go index 0c8cb7c..80aa8f2 100644 --- a/http/server.go +++ b/http/server.go @@ -64,6 +64,9 @@ func NewServer() *Server { router: mux.NewRouter(), } + // Report panics to external service. + s.router.Use(reportPanic) + // Our router is wrapped by another function handler to perform some // middleware-like tasks that cannot be performed by actual middleware. // This includes changing route paths for JSON endpoints & overridding methods. @@ -346,6 +349,20 @@ func loadFlash(next http.Handler) http.Handler { }) } +// reportPanic is middleware for catching panics and reporting them. +func reportPanic(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer func() { + if err := recover(); err != nil { + w.WriteHeader(http.StatusInternalServerError) + wtf.ReportPanic(err) + } + }() + + next.ServeHTTP(w, r) + }) +} + // handleNotFound handles requests to routes that don't exist. func (s *Server) handleNotFound(w http.ResponseWriter, r *http.Request) { tmpl := html.ErrorTemplate{ diff --git a/wtf.go b/wtf.go index 4183754..b7b1d37 100644 --- a/wtf.go +++ b/wtf.go @@ -1,7 +1,17 @@ package wtf +import ( + "context" +) + // Build version & commit SHA. var ( Version string Commit string ) + +// ReportError notifies an external service of errors. No-op by default. +var ReportError = func(ctx context.Context, err error, args ...interface{}) {} + +// ReportPanic notifies an external service of panics. No-op by default. +var ReportPanic = func(err interface{}) {}