From 540edc0c35e97393d9a01c927f7ac962bcbf6d37 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Sun, 26 Aug 2018 15:46:18 +1000 Subject: [PATCH] anonymous reporting data --- Gopkg.lock | 25 ++ docs/Config.md | 1 + pkg/app/app.go | 35 ++- pkg/commands/git.go | 6 +- pkg/commands/git_test.go | 6 +- pkg/commands/os.go | 4 +- pkg/config/app_config.go | 32 +-- pkg/git/branch_list_builder.go | 4 +- pkg/gui/gui.go | 20 +- pkg/i18n/english.go | 6 + pkg/i18n/i18n.go | 10 +- pkg/i18n/i18n_test.go | 10 +- vendor/github.com/heroku/rollrus/LICENSE | 22 ++ vendor/github.com/heroku/rollrus/rollrus.go | 287 ++++++++++++++++++++ vendor/github.com/pkg/errors/LICENSE | 23 ++ vendor/github.com/pkg/errors/errors.go | 269 ++++++++++++++++++ vendor/github.com/pkg/errors/stack.go | 178 ++++++++++++ vendor/github.com/stvp/roll/LICENSE | 22 ++ vendor/github.com/stvp/roll/client.go | 240 ++++++++++++++++ vendor/github.com/stvp/roll/stack.go | 98 +++++++ 20 files changed, 1256 insertions(+), 42 deletions(-) create mode 100644 vendor/github.com/heroku/rollrus/LICENSE create mode 100644 vendor/github.com/heroku/rollrus/rollrus.go create mode 100644 vendor/github.com/pkg/errors/LICENSE create mode 100644 vendor/github.com/pkg/errors/errors.go create mode 100644 vendor/github.com/pkg/errors/stack.go create mode 100644 vendor/github.com/stvp/roll/LICENSE create mode 100644 vendor/github.com/stvp/roll/client.go create mode 100644 vendor/github.com/stvp/roll/stack.go diff --git a/Gopkg.lock b/Gopkg.lock index bf05a0041..8842f1d6b 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -75,6 +75,14 @@ pruneopts = "NUT" revision = "ef8a98b0bbce4a65b5aa4c368430a80ddc533168" +[[projects]] + branch = "master" + digest = "1:d457d39e88f678ed14ac29517c3d74927a48dbc6a9f073fa241cf364a68cbe5c" + name = "github.com/heroku/rollrus" + packages = ["."] + pruneopts = "NUT" + revision = "fc0cef2ff331aebb24cd4e9ded7e20650f3d7006" + [[projects]] branch = "master" digest = "1:62fe3a7ea2050ecbd753a71889026f83d73329337ada66325cbafd5dea5f713d" @@ -191,6 +199,14 @@ revision = "c01d1270ff3e442a8a57cddc1c92dc1138598194" version = "v1.2.0" +[[projects]] + digest = "1:5cf3f025cbee5951a4ee961de067c8a89fc95a5adabead774f82822efabab121" + name = "github.com/pkg/errors" + packages = ["."] + pruneopts = "NUT" + revision = "645ef00459ed84a119197bfb8d8205042c6df63d" + version = "v0.8.0" + [[projects]] digest = "1:0028cb19b2e4c3112225cd871870f2d9cf49b9b4276531f03438a88e94be86fe" name = "github.com/pmezard/go-difflib" @@ -294,6 +310,14 @@ revision = "f35b8ab0b5a2cef36673838d662e249dd9c94686" version = "v1.2.2" +[[projects]] + branch = "master" + digest = "1:e42372d3f4921ec35df07f9b23239631e9d28580f7c1edcca212bc6daddc68fe" + name = "github.com/stvp/roll" + packages = ["."] + pruneopts = "NUT" + revision = "3627a5cbeaeaa68023abd02bb8687925265f2f63" + [[projects]] digest = "1:cd5ffc5bda4e0296ab3e4de90dbb415259c78e45e7fab13694b14cde8ab74541" name = "github.com/tcnksm/go-gitconfig" @@ -458,6 +482,7 @@ "github.com/davecgh/go-spew/spew", "github.com/fatih/color", "github.com/golang-collections/collections/stack", + "github.com/heroku/rollrus", "github.com/jesseduffield/gocui", "github.com/mgutz/str", "github.com/nicksnyder/go-i18n/v2/i18n", diff --git a/docs/Config.md b/docs/Config.md index 7e657adc4..35a05e539 100644 --- a/docs/Config.md +++ b/docs/Config.md @@ -18,6 +18,7 @@ # stuff relating to git os: # stuff relating to the OS + reporting: 'undetermined' # one of: 'on' | 'off' | 'undetermined' ``` ## Color Attributes: diff --git a/pkg/app/app.go b/pkg/app/app.go index 102ab4146..a558e1b19 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -5,11 +5,12 @@ import ( "io/ioutil" "os" - "github.com/sirupsen/logrus" + "github.com/heroku/rollrus" "github.com/jesseduffield/lazygit/pkg/commands" "github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/gui" "github.com/jesseduffield/lazygit/pkg/i18n" + "github.com/sirupsen/logrus" ) // App struct @@ -17,19 +18,26 @@ type App struct { closers []io.Closer Config config.AppConfigurer - Log *logrus.Logger + Log *logrus.Entry OSCommand *commands.OSCommand GitCommand *commands.GitCommand Gui *gui.Gui Tr *i18n.Localizer } -func newLogger(config config.AppConfigurer) *logrus.Logger { +func newProductionLogger(config config.AppConfigurer) *logrus.Logger { log := logrus.New() - if !config.GetDebug() { - log.Out = ioutil.Discard - return log + log.Out = ioutil.Discard + if config.GetUserConfig().GetString("reporting") == "on" { + // this isn't really a secret token: it only has permission to push new rollbar items + hook := rollrus.NewHook("23432119147a4367abf7c0de2aa99a2d", "production") + log.Hooks.Add(hook) } + return log +} + +func newDevelopmentLogger() *logrus.Logger { + log := logrus.New() file, err := os.OpenFile("development.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) if err != nil { panic("unable to log to file") // TODO: don't panic (also, remove this call to the `panic` function) @@ -38,6 +46,21 @@ func newLogger(config config.AppConfigurer) *logrus.Logger { return log } +func newLogger(config config.AppConfigurer) *logrus.Entry { + var log *logrus.Logger + if config.GetDebug() { + log = newDevelopmentLogger() + } else { + log = newProductionLogger(config) + } + return log.WithFields(logrus.Fields{ + "debug": config.GetDebug(), + "version": config.GetVersion(), + "commit": config.GetCommit(), + "buildDate": config.GetBuildDate(), + }) +} + // NewApp retruns a new applications func NewApp(config config.AppConfigurer) (*App, error) { app := &App{ diff --git a/pkg/commands/git.go b/pkg/commands/git.go index 14f3a433a..75de1fc70 100644 --- a/pkg/commands/git.go +++ b/pkg/commands/git.go @@ -7,23 +7,23 @@ import ( "os/exec" "strings" - "github.com/sirupsen/logrus" "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/utils" + "github.com/sirupsen/logrus" gitconfig "github.com/tcnksm/go-gitconfig" gogit "gopkg.in/src-d/go-git.v4" ) // GitCommand is our main git interface type GitCommand struct { - Log *logrus.Logger + Log *logrus.Entry OSCommand *OSCommand Worktree *gogit.Worktree Repo *gogit.Repository } // NewGitCommand it runs git commands -func NewGitCommand(log *logrus.Logger, osCommand *OSCommand) (*GitCommand, error) { +func NewGitCommand(log *logrus.Entry, osCommand *OSCommand) (*GitCommand, error) { gitCommand := &GitCommand{ Log: log, OSCommand: osCommand, diff --git a/pkg/commands/git_test.go b/pkg/commands/git_test.go index c930f76eb..ca1000cd2 100644 --- a/pkg/commands/git_test.go +++ b/pkg/commands/git_test.go @@ -5,14 +5,14 @@ import ( "strings" "testing" - "github.com/sirupsen/logrus" "github.com/jesseduffield/lazygit/pkg/test" + "github.com/sirupsen/logrus" ) -func getDummyLog() *logrus.Logger { +func getDummyLog() *logrus.Entry { log := logrus.New() log.Out = ioutil.Discard - return log + return log.WithField("test", "test") } func getDummyOSCommand() *OSCommand { diff --git a/pkg/commands/os.go b/pkg/commands/os.go index 1eef36151..0b490e995 100644 --- a/pkg/commands/os.go +++ b/pkg/commands/os.go @@ -25,12 +25,12 @@ type Platform struct { // OSCommand holds all the os commands type OSCommand struct { - Log *logrus.Logger + Log *logrus.Entry Platform *Platform } // NewOSCommand os command runner -func NewOSCommand(log *logrus.Logger) (*OSCommand, error) { +func NewOSCommand(log *logrus.Entry) (*OSCommand, error) { osCommand := &OSCommand{ Log: log, Platform: getPlatform(), diff --git a/pkg/config/app_config.go b/pkg/config/app_config.go index aa56365e3..d55393a41 100644 --- a/pkg/config/app_config.go +++ b/pkg/config/app_config.go @@ -141,22 +141,22 @@ func (c *AppConfig) InsertToUserConfig(key, value string) error { func getDefaultConfig() []byte { return []byte(` - gui: - ## stuff relating to the UI - scrollHeight: 2 - theme: - activeBorderColor: - - white - - bold - inactiveBorderColor: - - white - optionsTextColor: - - blue - git: - # stuff relating to git - os: - # stuff relating to the OS - +gui: + ## stuff relating to the UI + scrollHeight: 2 + theme: + activeBorderColor: + - white + - bold + inactiveBorderColor: + - white + optionsTextColor: + - blue +git: + # stuff relating to git +os: + # stuff relating to the OS +reporting: 'undetermined' # one of: 'on' | 'off' | 'undetermined' `) } diff --git a/pkg/git/branch_list_builder.go b/pkg/git/branch_list_builder.go index 37421d5b6..651b684f5 100644 --- a/pkg/git/branch_list_builder.go +++ b/pkg/git/branch_list_builder.go @@ -22,12 +22,12 @@ import ( // BranchListBuilder returns a list of Branch objects for the current repo type BranchListBuilder struct { - Log *logrus.Logger + Log *logrus.Entry GitCommand *commands.GitCommand } // NewBranchListBuilder builds a new branch list builder -func NewBranchListBuilder(log *logrus.Logger, gitCommand *commands.GitCommand) (*BranchListBuilder, error) { +func NewBranchListBuilder(log *logrus.Entry, gitCommand *commands.GitCommand) (*BranchListBuilder, error) { return &BranchListBuilder{ Log: log, GitCommand: gitCommand, diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 3d9c7b617..ae5e3bf29 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -15,12 +15,12 @@ import ( // "strings" - "github.com/sirupsen/logrus" "github.com/golang-collections/collections/stack" "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/commands" "github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/i18n" + "github.com/sirupsen/logrus" ) // OverlappingEdges determines if panel edges overlap @@ -56,7 +56,7 @@ type Teml i18n.Teml // Gui wraps the gocui Gui object which handles rendering and events type Gui struct { g *gocui.Gui - Log *logrus.Logger + Log *logrus.Entry GitCommand *commands.GitCommand OSCommand *commands.OSCommand SubProcess *exec.Cmd @@ -81,7 +81,7 @@ type guiState struct { } // NewGui builds a new gui handler -func NewGui(log *logrus.Logger, gitCommand *commands.GitCommand, oSCommand *commands.OSCommand, tr *i18n.Localizer, config config.AppConfigurer) (*Gui, error) { +func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *commands.OSCommand, tr *i18n.Localizer, config config.AppConfigurer) (*Gui, error) { initialState := guiState{ Files: make([]commands.File, 0), PreviousView: "files", @@ -270,6 +270,12 @@ func (gui *Gui) layout(g *gocui.Gui) error { if err := gui.switchFocus(g, nil, filesView); err != nil { return err } + + if gui.Config.GetUserConfig().GetString("reporting") == "undetermined" { + if err := gui.promptAnonymousReporting(); err != nil { + return err + } + } } gui.resizePopupPanels(g) @@ -277,6 +283,14 @@ func (gui *Gui) layout(g *gocui.Gui) error { return nil } +func (gui *Gui) promptAnonymousReporting() error { + return gui.createConfirmationPanel(gui.g, nil, gui.Tr.SLocalize("AnonymousReportingTitle"), gui.Tr.SLocalize("AnonymousReportingPrompt"), func(g *gocui.Gui, v *gocui.View) error { + return gui.Config.InsertToUserConfig("reporting", "on") + }, func(g *gocui.Gui, v *gocui.View) error { + return gui.Config.InsertToUserConfig("reporting", "off") + }) +} + func (gui *Gui) fetch(g *gocui.Gui) error { gui.GitCommand.Fetch() gui.refreshStatus(g) diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index 38fbac4cb..cd3438409 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -312,6 +312,12 @@ func addEnglish(i18nObject *i18n.Bundle) error { }, &i18n.Message{ ID: "ForcePushPrompt", Other: "Your branch has diverged from the remote branch. Press 'esc' to cancel, or 'enter' to force push.", + }, &i18n.Message{ + ID: "AnonymousReportingTitle", + Other: "Help make lazygit better", + }, &i18n.Message{ + ID: "AnonymousReportingPrompt", + Other: "Would you like to enable anonymous reporting data to help improve lazygit? (enter/esc)", }, ) } diff --git a/pkg/i18n/i18n.go b/pkg/i18n/i18n.go index 898a13906..1628df707 100644 --- a/pkg/i18n/i18n.go +++ b/pkg/i18n/i18n.go @@ -1,9 +1,9 @@ package i18n import ( - "github.com/sirupsen/logrus" "github.com/cloudfoundry/jibber_jabber" "github.com/nicksnyder/go-i18n/v2/i18n" + "github.com/sirupsen/logrus" "golang.org/x/text/language" ) @@ -14,11 +14,11 @@ type Teml map[string]interface{} type Localizer struct { i18nLocalizer *i18n.Localizer language string - Log *logrus.Logger + Log *logrus.Entry } // NewLocalizer creates a new Localizer -func NewLocalizer(log *logrus.Logger) *Localizer { +func NewLocalizer(log *logrus.Entry) *Localizer { userLang := detectLanguage(jibber_jabber.DetectLanguage) log.Info("language: " + userLang) @@ -60,7 +60,7 @@ func (l *Localizer) GetLanguage() string { } // add translation file(s) -func addBundles(log *logrus.Logger, i18nBundle *i18n.Bundle) { +func addBundles(log *logrus.Entry, i18nBundle *i18n.Bundle) { fs := []func(*i18n.Bundle) error{ addPolish, addDutch, @@ -85,7 +85,7 @@ func detectLanguage(langDetector func() (string, error)) string { } // setupLocalizer creates a new localizer using given userLang -func setupLocalizer(log *logrus.Logger, userLang string) *Localizer { +func setupLocalizer(log *logrus.Entry, userLang string) *Localizer { // create a i18n bundle that can be used to add translations and other things i18nBundle := &i18n.Bundle{DefaultLanguage: language.English} diff --git a/pkg/i18n/i18n_test.go b/pkg/i18n/i18n_test.go index 5eeddb907..e26b7d1dc 100644 --- a/pkg/i18n/i18n_test.go +++ b/pkg/i18n/i18n_test.go @@ -2,6 +2,7 @@ package i18n import ( "fmt" + "io/ioutil" "testing" "github.com/nicksnyder/go-i18n/v2/i18n" @@ -10,8 +11,13 @@ import ( "github.com/stretchr/testify/assert" ) +func getDummyLog() *logrus.Entry { + log := logrus.New() + log.Out = ioutil.Discard + return log.WithField("test", "test") +} func TestNewLocalizer(t *testing.T) { - assert.NotNil(t, NewLocalizer(logrus.New())) + assert.NotNil(t, NewLocalizer(getDummyLog())) } func TestDetectLanguage(t *testing.T) { @@ -76,6 +82,6 @@ func TestLocalizer(t *testing.T) { } for _, s := range scenarios { - s.test(setupLocalizer(logrus.New(), s.userLang)) + s.test(setupLocalizer(getDummyLog(), s.userLang)) } } diff --git a/vendor/github.com/heroku/rollrus/LICENSE b/vendor/github.com/heroku/rollrus/LICENSE new file mode 100644 index 000000000..8d4a5174d --- /dev/null +++ b/vendor/github.com/heroku/rollrus/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright © Heroku 2014 - 2015 + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/heroku/rollrus/rollrus.go b/vendor/github.com/heroku/rollrus/rollrus.go new file mode 100644 index 000000000..50dd225da --- /dev/null +++ b/vendor/github.com/heroku/rollrus/rollrus.go @@ -0,0 +1,287 @@ +// Package rollrus combines github.com/stvp/roll with github.com/sirupsen/logrus +// via logrus.Hook mechanism, so that whenever logrus' logger.Error/f(), +// logger.Fatal/f() or logger.Panic/f() are used the messages are +// intercepted and sent to rollbar. +// +// Using SetupLogging should suffice for basic use cases that use the logrus +// singleton logger. +// +// More custom uses are supported by creating a new Hook with NewHook and +// registering that hook with the logrus Logger of choice. +// +// The levels can be customized with the WithLevels OptionFunc. +// +// Specific errors can be ignored with the WithIgnoredErrors OptionFunc. This is +// useful for ignoring errors such as context.Canceled. +// +// See the Examples in the tests for more usage. +package rollrus + +import ( + "fmt" + "os" + "time" + + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "github.com/stvp/roll" +) + +var defaultTriggerLevels = []logrus.Level{ + logrus.ErrorLevel, + logrus.FatalLevel, + logrus.PanicLevel, +} + +// Hook is a wrapper for the rollbar Client and is usable as a logrus.Hook. +type Hook struct { + roll.Client + triggers []logrus.Level + ignoredErrors map[error]struct{} + ignoreErrorFunc func(error) bool + ignoreFunc func(error, map[string]string) bool + + // only used for tests to verify whether or not a report happened. + reported bool +} + +// OptionFunc that can be passed to NewHook. +type OptionFunc func(*Hook) + +// wellKnownErrorFields are the names of the fields to be checked for values of +// type `error`, in priority order. +var wellKnownErrorFields = []string{ + logrus.ErrorKey, "err", +} + +// WithLevels is an OptionFunc that customizes the log.Levels the hook will +// report on. +func WithLevels(levels ...logrus.Level) OptionFunc { + return func(h *Hook) { + h.triggers = levels + } +} + +// WithMinLevel is an OptionFunc that customizes the log.Levels the hook will +// report on by selecting all levels more severe than the one provided. +func WithMinLevel(level logrus.Level) OptionFunc { + var levels []logrus.Level + for _, l := range logrus.AllLevels { + if l <= level { + levels = append(levels, l) + } + } + + return func(h *Hook) { + h.triggers = levels + } +} + +// WithIgnoredErrors is an OptionFunc that whitelists certain errors to prevent +// them from firing. +func WithIgnoredErrors(errors ...error) OptionFunc { + return func(h *Hook) { + for _, e := range errors { + h.ignoredErrors[e] = struct{}{} + } + } +} + +// WithIgnoreErrorFunc is an OptionFunc that receives the error that is about +// to be logged and returns true/false if it wants to fire a rollbar alert for. +func WithIgnoreErrorFunc(fn func(error) bool) OptionFunc { + return func(h *Hook) { + h.ignoreErrorFunc = fn + } +} + +// WithIgnoreFunc is an OptionFunc that receives the error and custom fields that are about +// to be logged and returns true/false if it wants to fire a rollbar alert for. +func WithIgnoreFunc(fn func(err error, fields map[string]string) bool) OptionFunc { + return func(h *Hook) { + h.ignoreFunc = fn + } +} + +// NewHook creates a hook that is intended for use with your own logrus.Logger +// instance. Uses the defualt report levels defined in wellKnownErrorFields. +func NewHook(token string, env string, opts ...OptionFunc) *Hook { + h := NewHookForLevels(token, env, defaultTriggerLevels) + + for _, o := range opts { + o(h) + } + + return h +} + +// NewHookForLevels provided by the caller. Otherwise works like NewHook. +func NewHookForLevels(token string, env string, levels []logrus.Level) *Hook { + return &Hook{ + Client: roll.New(token, env), + triggers: levels, + ignoredErrors: make(map[error]struct{}), + ignoreErrorFunc: func(error) bool { return false }, + ignoreFunc: func(error, map[string]string) bool { return false }, + } +} + +// SetupLogging for use on Heroku. If token is not an empty string a rollbar +// hook is added with the environment set to env. The log formatter is set to a +// TextFormatter with timestamps disabled. +func SetupLogging(token, env string) { + setupLogging(token, env, defaultTriggerLevels) +} + +// SetupLoggingForLevels works like SetupLogging, but allows you to +// set the levels on which to trigger this hook. +func SetupLoggingForLevels(token, env string, levels []logrus.Level) { + setupLogging(token, env, levels) +} + +func setupLogging(token, env string, levels []logrus.Level) { + logrus.SetFormatter(&logrus.TextFormatter{DisableTimestamp: true}) + + if token != "" { + logrus.AddHook(NewHookForLevels(token, env, levels)) + } +} + +// ReportPanic attempts to report the panic to rollbar using the provided +// client and then re-panic. If it can't report the panic it will print an +// error to stderr. +func (r *Hook) ReportPanic() { + if p := recover(); p != nil { + if _, err := r.Client.Critical(fmt.Errorf("panic: %q", p), nil); err != nil { + fmt.Fprintf(os.Stderr, "reporting_panic=false err=%q\n", err) + } + panic(p) + } +} + +// ReportPanic attempts to report the panic to rollbar if the token is set +func ReportPanic(token, env string) { + if token != "" { + h := &Hook{Client: roll.New(token, env)} + h.ReportPanic() + } +} + +// Levels returns the logrus log.Levels that this hook handles +func (r *Hook) Levels() []logrus.Level { + if r.triggers == nil { + return defaultTriggerLevels + } + return r.triggers +} + +// Fire the hook. This is called by Logrus for entries that match the levels +// returned by Levels(). +func (r *Hook) Fire(entry *logrus.Entry) error { + trace, cause := extractError(entry) + if _, ok := r.ignoredErrors[cause]; ok { + return nil + } + + if r.ignoreErrorFunc(cause) { + return nil + } + + m := convertFields(entry.Data) + if _, exists := m["time"]; !exists { + m["time"] = entry.Time.Format(time.RFC3339) + } + + if r.ignoreFunc(cause, m) { + return nil + } + + return r.report(entry, cause, m, trace) +} + +func (r *Hook) report(entry *logrus.Entry, cause error, m map[string]string, trace []uintptr) (err error) { + hasTrace := len(trace) > 0 + level := entry.Level + + r.reported = true + + switch { + case hasTrace && level == logrus.FatalLevel: + _, err = r.Client.CriticalStack(cause, trace, m) + case hasTrace && level == logrus.PanicLevel: + _, err = r.Client.CriticalStack(cause, trace, m) + case hasTrace && level == logrus.ErrorLevel: + _, err = r.Client.ErrorStack(cause, trace, m) + case hasTrace && level == logrus.WarnLevel: + _, err = r.Client.WarningStack(cause, trace, m) + case level == logrus.FatalLevel || level == logrus.PanicLevel: + _, err = r.Client.Critical(cause, m) + case level == logrus.ErrorLevel: + _, err = r.Client.Error(cause, m) + case level == logrus.WarnLevel: + _, err = r.Client.Warning(cause, m) + case level == logrus.InfoLevel: + _, err = r.Client.Info(entry.Message, m) + case level == logrus.DebugLevel: + _, err = r.Client.Debug(entry.Message, m) + } + return err +} + +// convertFields converts from log.Fields to map[string]string so that we can +// report extra fields to Rollbar +func convertFields(fields logrus.Fields) map[string]string { + m := make(map[string]string) + for k, v := range fields { + switch t := v.(type) { + case time.Time: + m[k] = t.Format(time.RFC3339) + default: + if s, ok := v.(fmt.Stringer); ok { + m[k] = s.String() + } else { + m[k] = fmt.Sprintf("%+v", t) + } + } + } + + return m +} + +// extractError attempts to extract an error from a well known field, err or error +func extractError(entry *logrus.Entry) ([]uintptr, error) { + var trace []uintptr + fields := entry.Data + + type stackTracer interface { + StackTrace() errors.StackTrace + } + + for _, f := range wellKnownErrorFields { + e, ok := fields[f] + if !ok { + continue + } + err, ok := e.(error) + if !ok { + continue + } + + cause := errors.Cause(err) + tracer, ok := err.(stackTracer) + if ok { + return copyStackTrace(tracer.StackTrace()), cause + } + return trace, cause + } + + // when no error found, default to the logged message. + return trace, fmt.Errorf(entry.Message) +} + +func copyStackTrace(trace errors.StackTrace) (out []uintptr) { + for _, frame := range trace { + out = append(out, uintptr(frame)) + } + return +} diff --git a/vendor/github.com/pkg/errors/LICENSE b/vendor/github.com/pkg/errors/LICENSE new file mode 100644 index 000000000..835ba3e75 --- /dev/null +++ b/vendor/github.com/pkg/errors/LICENSE @@ -0,0 +1,23 @@ +Copyright (c) 2015, Dave Cheney +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/pkg/errors/errors.go b/vendor/github.com/pkg/errors/errors.go new file mode 100644 index 000000000..842ee8045 --- /dev/null +++ b/vendor/github.com/pkg/errors/errors.go @@ -0,0 +1,269 @@ +// Package errors provides simple error handling primitives. +// +// The traditional error handling idiom in Go is roughly akin to +// +// if err != nil { +// return err +// } +// +// which applied recursively up the call stack results in error reports +// without context or debugging information. The errors package allows +// programmers to add context to the failure path in their code in a way +// that does not destroy the original value of the error. +// +// Adding context to an error +// +// The errors.Wrap function returns a new error that adds context to the +// original error by recording a stack trace at the point Wrap is called, +// and the supplied message. For example +// +// _, err := ioutil.ReadAll(r) +// if err != nil { +// return errors.Wrap(err, "read failed") +// } +// +// If additional control is required the errors.WithStack and errors.WithMessage +// functions destructure errors.Wrap into its component operations of annotating +// an error with a stack trace and an a message, respectively. +// +// Retrieving the cause of an error +// +// Using errors.Wrap constructs a stack of errors, adding context to the +// preceding error. Depending on the nature of the error it may be necessary +// to reverse the operation of errors.Wrap to retrieve the original error +// for inspection. Any error value which implements this interface +// +// type causer interface { +// Cause() error +// } +// +// can be inspected by errors.Cause. errors.Cause will recursively retrieve +// the topmost error which does not implement causer, which is assumed to be +// the original cause. For example: +// +// switch err := errors.Cause(err).(type) { +// case *MyError: +// // handle specifically +// default: +// // unknown error +// } +// +// causer interface is not exported by this package, but is considered a part +// of stable public API. +// +// Formatted printing of errors +// +// All error values returned from this package implement fmt.Formatter and can +// be formatted by the fmt package. The following verbs are supported +// +// %s print the error. If the error has a Cause it will be +// printed recursively +// %v see %s +// %+v extended format. Each Frame of the error's StackTrace will +// be printed in detail. +// +// Retrieving the stack trace of an error or wrapper +// +// New, Errorf, Wrap, and Wrapf record a stack trace at the point they are +// invoked. This information can be retrieved with the following interface. +// +// type stackTracer interface { +// StackTrace() errors.StackTrace +// } +// +// Where errors.StackTrace is defined as +// +// type StackTrace []Frame +// +// The Frame type represents a call site in the stack trace. Frame supports +// the fmt.Formatter interface that can be used for printing information about +// the stack trace of this error. For example: +// +// if err, ok := err.(stackTracer); ok { +// for _, f := range err.StackTrace() { +// fmt.Printf("%+s:%d", f) +// } +// } +// +// stackTracer interface is not exported by this package, but is considered a part +// of stable public API. +// +// See the documentation for Frame.Format for more details. +package errors + +import ( + "fmt" + "io" +) + +// New returns an error with the supplied message. +// New also records the stack trace at the point it was called. +func New(message string) error { + return &fundamental{ + msg: message, + stack: callers(), + } +} + +// Errorf formats according to a format specifier and returns the string +// as a value that satisfies error. +// Errorf also records the stack trace at the point it was called. +func Errorf(format string, args ...interface{}) error { + return &fundamental{ + msg: fmt.Sprintf(format, args...), + stack: callers(), + } +} + +// fundamental is an error that has a message and a stack, but no caller. +type fundamental struct { + msg string + *stack +} + +func (f *fundamental) Error() string { return f.msg } + +func (f *fundamental) Format(s fmt.State, verb rune) { + switch verb { + case 'v': + if s.Flag('+') { + io.WriteString(s, f.msg) + f.stack.Format(s, verb) + return + } + fallthrough + case 's': + io.WriteString(s, f.msg) + case 'q': + fmt.Fprintf(s, "%q", f.msg) + } +} + +// WithStack annotates err with a stack trace at the point WithStack was called. +// If err is nil, WithStack returns nil. +func WithStack(err error) error { + if err == nil { + return nil + } + return &withStack{ + err, + callers(), + } +} + +type withStack struct { + error + *stack +} + +func (w *withStack) Cause() error { return w.error } + +func (w *withStack) Format(s fmt.State, verb rune) { + switch verb { + case 'v': + if s.Flag('+') { + fmt.Fprintf(s, "%+v", w.Cause()) + w.stack.Format(s, verb) + return + } + fallthrough + case 's': + io.WriteString(s, w.Error()) + case 'q': + fmt.Fprintf(s, "%q", w.Error()) + } +} + +// Wrap returns an error annotating err with a stack trace +// at the point Wrap is called, and the supplied message. +// If err is nil, Wrap returns nil. +func Wrap(err error, message string) error { + if err == nil { + return nil + } + err = &withMessage{ + cause: err, + msg: message, + } + return &withStack{ + err, + callers(), + } +} + +// Wrapf returns an error annotating err with a stack trace +// at the point Wrapf is call, and the format specifier. +// If err is nil, Wrapf returns nil. +func Wrapf(err error, format string, args ...interface{}) error { + if err == nil { + return nil + } + err = &withMessage{ + cause: err, + msg: fmt.Sprintf(format, args...), + } + return &withStack{ + err, + callers(), + } +} + +// WithMessage annotates err with a new message. +// If err is nil, WithMessage returns nil. +func WithMessage(err error, message string) error { + if err == nil { + return nil + } + return &withMessage{ + cause: err, + msg: message, + } +} + +type withMessage struct { + cause error + msg string +} + +func (w *withMessage) Error() string { return w.msg + ": " + w.cause.Error() } +func (w *withMessage) Cause() error { return w.cause } + +func (w *withMessage) Format(s fmt.State, verb rune) { + switch verb { + case 'v': + if s.Flag('+') { + fmt.Fprintf(s, "%+v\n", w.Cause()) + io.WriteString(s, w.msg) + return + } + fallthrough + case 's', 'q': + io.WriteString(s, w.Error()) + } +} + +// Cause returns the underlying cause of the error, if possible. +// An error value has a cause if it implements the following +// interface: +// +// type causer interface { +// Cause() error +// } +// +// If the error does not implement Cause, the original error will +// be returned. If the error is nil, nil will be returned without further +// investigation. +func Cause(err error) error { + type causer interface { + Cause() error + } + + for err != nil { + cause, ok := err.(causer) + if !ok { + break + } + err = cause.Cause() + } + return err +} diff --git a/vendor/github.com/pkg/errors/stack.go b/vendor/github.com/pkg/errors/stack.go new file mode 100644 index 000000000..6b1f2891a --- /dev/null +++ b/vendor/github.com/pkg/errors/stack.go @@ -0,0 +1,178 @@ +package errors + +import ( + "fmt" + "io" + "path" + "runtime" + "strings" +) + +// Frame represents a program counter inside a stack frame. +type Frame uintptr + +// pc returns the program counter for this frame; +// multiple frames may have the same PC value. +func (f Frame) pc() uintptr { return uintptr(f) - 1 } + +// file returns the full path to the file that contains the +// function for this Frame's pc. +func (f Frame) file() string { + fn := runtime.FuncForPC(f.pc()) + if fn == nil { + return "unknown" + } + file, _ := fn.FileLine(f.pc()) + return file +} + +// line returns the line number of source code of the +// function for this Frame's pc. +func (f Frame) line() int { + fn := runtime.FuncForPC(f.pc()) + if fn == nil { + return 0 + } + _, line := fn.FileLine(f.pc()) + return line +} + +// Format formats the frame according to the fmt.Formatter interface. +// +// %s source file +// %d source line +// %n function name +// %v equivalent to %s:%d +// +// Format accepts flags that alter the printing of some verbs, as follows: +// +// %+s path of source file relative to the compile time GOPATH +// %+v equivalent to %+s:%d +func (f Frame) Format(s fmt.State, verb rune) { + switch verb { + case 's': + switch { + case s.Flag('+'): + pc := f.pc() + fn := runtime.FuncForPC(pc) + if fn == nil { + io.WriteString(s, "unknown") + } else { + file, _ := fn.FileLine(pc) + fmt.Fprintf(s, "%s\n\t%s", fn.Name(), file) + } + default: + io.WriteString(s, path.Base(f.file())) + } + case 'd': + fmt.Fprintf(s, "%d", f.line()) + case 'n': + name := runtime.FuncForPC(f.pc()).Name() + io.WriteString(s, funcname(name)) + case 'v': + f.Format(s, 's') + io.WriteString(s, ":") + f.Format(s, 'd') + } +} + +// StackTrace is stack of Frames from innermost (newest) to outermost (oldest). +type StackTrace []Frame + +func (st StackTrace) Format(s fmt.State, verb rune) { + switch verb { + case 'v': + switch { + case s.Flag('+'): + for _, f := range st { + fmt.Fprintf(s, "\n%+v", f) + } + case s.Flag('#'): + fmt.Fprintf(s, "%#v", []Frame(st)) + default: + fmt.Fprintf(s, "%v", []Frame(st)) + } + case 's': + fmt.Fprintf(s, "%s", []Frame(st)) + } +} + +// stack represents a stack of program counters. +type stack []uintptr + +func (s *stack) Format(st fmt.State, verb rune) { + switch verb { + case 'v': + switch { + case st.Flag('+'): + for _, pc := range *s { + f := Frame(pc) + fmt.Fprintf(st, "\n%+v", f) + } + } + } +} + +func (s *stack) StackTrace() StackTrace { + f := make([]Frame, len(*s)) + for i := 0; i < len(f); i++ { + f[i] = Frame((*s)[i]) + } + return f +} + +func callers() *stack { + const depth = 32 + var pcs [depth]uintptr + n := runtime.Callers(3, pcs[:]) + var st stack = pcs[0:n] + return &st +} + +// funcname removes the path prefix component of a function's name reported by func.Name(). +func funcname(name string) string { + i := strings.LastIndex(name, "/") + name = name[i+1:] + i = strings.Index(name, ".") + return name[i+1:] +} + +func trimGOPATH(name, file string) string { + // Here we want to get the source file path relative to the compile time + // GOPATH. As of Go 1.6.x there is no direct way to know the compiled + // GOPATH at runtime, but we can infer the number of path segments in the + // GOPATH. We note that fn.Name() returns the function name qualified by + // the import path, which does not include the GOPATH. Thus we can trim + // segments from the beginning of the file path until the number of path + // separators remaining is one more than the number of path separators in + // the function name. For example, given: + // + // GOPATH /home/user + // file /home/user/src/pkg/sub/file.go + // fn.Name() pkg/sub.Type.Method + // + // We want to produce: + // + // pkg/sub/file.go + // + // From this we can easily see that fn.Name() has one less path separator + // than our desired output. We count separators from the end of the file + // path until it finds two more than in the function name and then move + // one character forward to preserve the initial path segment without a + // leading separator. + const sep = "/" + goal := strings.Count(name, sep) + 2 + i := len(file) + for n := 0; n < goal; n++ { + i = strings.LastIndex(file[:i], sep) + if i == -1 { + // not enough separators found, set i so that the slice expression + // below leaves file unmodified + i = -len(sep) + break + } + } + // get back to 0 or trim the leading separator + file = file[i+len(sep):] + return file +} diff --git a/vendor/github.com/stvp/roll/LICENSE b/vendor/github.com/stvp/roll/LICENSE new file mode 100644 index 000000000..1235e06b5 --- /dev/null +++ b/vendor/github.com/stvp/roll/LICENSE @@ -0,0 +1,22 @@ +Copyright (c) 2016 Stovepipe Studios + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/stvp/roll/client.go b/vendor/github.com/stvp/roll/client.go new file mode 100644 index 000000000..687343f2a --- /dev/null +++ b/vendor/github.com/stvp/roll/client.go @@ -0,0 +1,240 @@ +package roll + +import ( + "bytes" + "encoding/json" + "fmt" + "hash/adler32" + "io" + "io/ioutil" + "net/http" + "os" + "reflect" + "runtime" + "strings" + "time" +) + +const ( + // By default, all Rollbar API requests are sent to this endpoint. + endpoint = "https://api.rollbar.com/api/1/item/" + + // Identify this Rollbar client library to the Rollbar API. + clientName = "go-roll" + clientVersion = "0.2.0" + clientLanguage = "go" +) + +var ( + // Endpoint is the default HTTP(S) endpoint that all Rollbar API requests + // will be sent to. By default, this is Rollbar's "Items" API endpoint. If + // this is blank, no items will be sent to Rollbar. + Endpoint = endpoint + + // Rollbar access token for the global client. If this is blank, no items + // will be sent to Rollbar. + Token = "" + + // Environment for all items reported with the global client. + Environment = "development" +) + +type rollbarSuccess struct { + Result map[string]string `json:"result"` +} + +// Client reports items to a single Rollbar project. +type Client interface { + Critical(err error, custom map[string]string) (uuid string, e error) + CriticalStack(err error, ptrs []uintptr, custom map[string]string) (uuid string, e error) + Error(err error, custom map[string]string) (uuid string, e error) + ErrorStack(err error, ptrs []uintptr, custom map[string]string) (uuid string, e error) + Warning(err error, custom map[string]string) (uuid string, e error) + WarningStack(err error, ptrs []uintptr, custom map[string]string) (uuid string, e error) + Info(msg string, custom map[string]string) (uuid string, e error) + Debug(msg string, custom map[string]string) (uuid string, e error) +} + +type rollbarClient struct { + token string + env string +} + +// New creates a new Rollbar client that reports items to the given project +// token and with the given environment (eg. "production", "development", etc). +func New(token, env string) Client { + return &rollbarClient{token, env} +} + +func Critical(err error, custom map[string]string) (uuid string, e error) { + return CriticalStack(err, getCallers(2), custom) +} + +func CriticalStack(err error, ptrs []uintptr, custom map[string]string) (uuid string, e error) { + return New(Token, Environment).CriticalStack(err, ptrs, custom) +} + +func Error(err error, custom map[string]string) (uuid string, e error) { + return ErrorStack(err, getCallers(2), custom) +} + +func ErrorStack(err error, ptrs []uintptr, custom map[string]string) (uuid string, e error) { + return New(Token, Environment).ErrorStack(err, ptrs, custom) +} + +func Warning(err error, custom map[string]string) (uuid string, e error) { + return WarningStack(err, getCallers(2), custom) +} + +func WarningStack(err error, ptrs []uintptr, custom map[string]string) (uuid string, e error) { + return New(Token, Environment).WarningStack(err, ptrs, custom) +} + +func Info(msg string, custom map[string]string) (uuid string, e error) { + return New(Token, Environment).Info(msg, custom) +} + +func Debug(msg string, custom map[string]string) (uuid string, e error) { + return New(Token, Environment).Debug(msg, custom) +} + +func (c *rollbarClient) Critical(err error, custom map[string]string) (uuid string, e error) { + return c.CriticalStack(err, getCallers(2), custom) +} + +func (c *rollbarClient) CriticalStack(err error, callers []uintptr, custom map[string]string) (uuid string, e error) { + item := c.buildTraceItem("critical", err, callers, custom) + return c.send(item) +} + +func (c *rollbarClient) Error(err error, custom map[string]string) (uuid string, e error) { + return c.ErrorStack(err, getCallers(2), custom) +} + +func (c *rollbarClient) ErrorStack(err error, callers []uintptr, custom map[string]string) (uuid string, e error) { + item := c.buildTraceItem("error", err, callers, custom) + return c.send(item) +} + +func (c *rollbarClient) Warning(err error, custom map[string]string) (uuid string, e error) { + return c.WarningStack(err, getCallers(2), custom) +} + +func (c *rollbarClient) WarningStack(err error, callers []uintptr, custom map[string]string) (uuid string, e error) { + item := c.buildTraceItem("warning", err, callers, custom) + return c.send(item) +} + +func (c *rollbarClient) Info(msg string, custom map[string]string) (uuid string, e error) { + item := c.buildMessageItem("info", msg, custom) + return c.send(item) +} + +func (c *rollbarClient) Debug(msg string, custom map[string]string) (uuid string, e error) { + item := c.buildMessageItem("debug", msg, custom) + return c.send(item) +} + +func (c *rollbarClient) buildTraceItem(level string, err error, callers []uintptr, custom map[string]string) (item map[string]interface{}) { + stack := buildRollbarFrames(callers) + item = c.buildItem(level, err.Error(), custom) + itemData := item["data"].(map[string]interface{}) + itemData["fingerprint"] = stack.fingerprint() + itemData["body"] = map[string]interface{}{ + "trace": map[string]interface{}{ + "frames": stack, + "exception": map[string]interface{}{ + "class": errorClass(err), + "message": err.Error(), + }, + }, + } + + return item +} + +func (c *rollbarClient) buildMessageItem(level string, msg string, custom map[string]string) (item map[string]interface{}) { + item = c.buildItem(level, msg, custom) + itemData := item["data"].(map[string]interface{}) + itemData["body"] = map[string]interface{}{ + "message": map[string]interface{}{ + "body": msg, + }, + } + + return item +} + +func (c *rollbarClient) buildItem(level, title string, custom map[string]string) map[string]interface{} { + hostname, _ := os.Hostname() + + return map[string]interface{}{ + "access_token": c.token, + "data": map[string]interface{}{ + "environment": c.env, + "title": title, + "level": level, + "timestamp": time.Now().Unix(), + "platform": runtime.GOOS, + "language": clientLanguage, + "server": map[string]interface{}{ + "host": hostname, + }, + "notifier": map[string]interface{}{ + "name": clientName, + "version": clientVersion, + }, + "custom": custom, + }, + } +} + +// send reports the given item to Rollbar and returns either a UUID for the +// reported item or an error. +func (c *rollbarClient) send(item map[string]interface{}) (uuid string, err error) { + if len(c.token) == 0 || len(Endpoint) == 0 { + return "", nil + } + + jsonBody, err := json.Marshal(item) + if err != nil { + return "", err + } + + resp, err := http.Post(Endpoint, "application/json", bytes.NewReader(jsonBody)) + if err != nil { + return "", err + } + defer func() { + io.Copy(ioutil.Discard, resp.Body) + resp.Body.Close() + }() + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("Rollbar returned %s", resp.Status) + } + + // Extract UUID from JSON response + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", nil + } + success := rollbarSuccess{} + json.Unmarshal(body, &success) + + return success.Result["uuid"], nil +} + +// errorClass returns a class name for an error (eg. "ErrUnexpectedEOF"). For +// string errors, it returns an Adler-32 checksum of the error string. +func errorClass(err error) string { + class := reflect.TypeOf(err).String() + if class == "" { + return "panic" + } else if class == "*errors.errorString" { + checksum := adler32.Checksum([]byte(err.Error())) + return fmt.Sprintf("{%x}", checksum) + } else { + return strings.TrimPrefix(class, "*") + } +} diff --git a/vendor/github.com/stvp/roll/stack.go b/vendor/github.com/stvp/roll/stack.go new file mode 100644 index 000000000..c50425491 --- /dev/null +++ b/vendor/github.com/stvp/roll/stack.go @@ -0,0 +1,98 @@ +package roll + +import ( + "fmt" + "hash/crc32" + "os" + "runtime" + "strings" +) + +var ( + knownFilePathPatterns = []string{ + "github.com/", + "code.google.com/", + "bitbucket.org/", + "launchpad.net/", + "gopkg.in/", + } +) + +func getCallers(skip int) (pc []uintptr) { + pc = make([]uintptr, 1000) + i := runtime.Callers(skip+1, pc) + return pc[0:i] +} + +// -- rollbarFrames + +type rollbarFrame struct { + Filename string `json:"filename"` + Method string `json:"method"` + Line int `json:"lineno"` +} + +type rollbarFrames []rollbarFrame + +// buildRollbarFrames takes a slice of function pointers and returns a Rollbar +// API payload containing the filename, method name, and line number of each +// function. +func buildRollbarFrames(callers []uintptr) (frames rollbarFrames) { + frames = rollbarFrames{} + + // 2016-08-24 - runtime.CallersFrames was added in Go 1.7, which should + // replace the following code when roll is able to require Go 1.7+. + for _, caller := range callers { + frame := rollbarFrame{ + Filename: "???", + Method: "???", + } + if fn := runtime.FuncForPC(caller); fn != nil { + name, line := fn.FileLine(caller) + frame.Filename = scrubFile(name) + frame.Line = line + frame.Method = scrubFunction(fn.Name()) + } + frames = append(frames, frame) + } + + return frames +} + +// fingerprint returns a checksum that uniquely identifies a stacktrace by the +// filename, method name, and line number of every frame in the stack. +func (f rollbarFrames) fingerprint() string { + hash := crc32.NewIEEE() + for _, frame := range f { + fmt.Fprintf(hash, "%s%s%d", frame.Filename, frame.Method, frame.Line) + } + return fmt.Sprintf("%x", hash.Sum32()) +} + +// -- Helpers + +// scrubFile removes unneeded information from the path of a source file. This +// makes them shorter in Rollbar UI as well as making them the same, regardless +// of the machine the code was compiled on. +// +// Example: +// /home/foo/go/src/github.com/stvp/roll/rollbar.go -> github.com/stvp/roll/rollbar.go +func scrubFile(s string) string { + var i int + for _, pattern := range knownFilePathPatterns { + i = strings.Index(s, pattern) + if i != -1 { + return s[i:] + } + } + return s +} + +// scrubFunction removes unneeded information from the full name of a function. +// +// Example: +// github.com/stvp/roll.getCallers -> roll.getCallers +func scrubFunction(name string) string { + end := strings.LastIndex(name, string(os.PathSeparator)) + return name[end+1 : len(name)] +}