From 1de2e5626fe81d5f3a0995fdd2141735b7ddbee7 Mon Sep 17 00:00:00 2001 From: Neur0toxine Date: Mon, 3 Aug 2020 14:45:16 +0300 Subject: [PATCH] Better stacktrace generation system, better tests for Sentry, new badge --- .travis.yml | 3 + README.md | 2 +- core/error.go | 5 + core/job_manager_test.go | 2 +- core/localizer.go | 8 +- core/sentry.go | 117 +------- core/sentry_test.go | 272 ++++++++++++++++-- core/stacktrace/abstract_stack_builder.go | 52 ++++ .../stacktrace/abstract_stack_builder_test.go | 60 ++++ core/stacktrace/generic_stack_builder.go | 21 ++ core/stacktrace/generic_stack_builder_test.go | 32 +++ core/stacktrace/pkg_errors_builder.go | 88 ++++++ core/stacktrace/pkg_errors_builder_test.go | 101 +++++++ core/stacktrace/raven_client_interface.go | 10 + core/stacktrace/raven_stacktrace_builder.go | 69 +++++ .../raven_stacktrace_builder_test.go | 55 ++++ core/stacktrace/stack_builder_factory.go | 22 ++ core/stacktrace/stack_builder_factory_test.go | 27 ++ core/stacktrace/unwrap_builder.go | 56 ++++ core/stacktrace/unwrap_builder_test.go | 88 ++++++ go.mod | 2 +- go.sum | 29 +- 22 files changed, 959 insertions(+), 162 deletions(-) create mode 100644 core/stacktrace/abstract_stack_builder.go create mode 100644 core/stacktrace/abstract_stack_builder_test.go create mode 100644 core/stacktrace/generic_stack_builder.go create mode 100644 core/stacktrace/generic_stack_builder_test.go create mode 100644 core/stacktrace/pkg_errors_builder.go create mode 100644 core/stacktrace/pkg_errors_builder_test.go create mode 100644 core/stacktrace/raven_client_interface.go create mode 100644 core/stacktrace/raven_stacktrace_builder.go create mode 100644 core/stacktrace/raven_stacktrace_builder_test.go create mode 100644 core/stacktrace/stack_builder_factory.go create mode 100644 core/stacktrace/stack_builder_factory_test.go create mode 100644 core/stacktrace/unwrap_builder.go create mode 100644 core/stacktrace/unwrap_builder_test.go diff --git a/.travis.yml b/.travis.yml index 468d092..8c4f415 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,9 +4,12 @@ env: go: - '1.12' - '1.13' + - '1.14' before_install: - go mod tidy script: - go test ./... -v -cpu 2 -timeout 2m -race -cover -coverprofile=coverage.txt -covermode=atomic + - go get -v -u github.com/axw/gocov/gocov + - gocov convert ./coverage.txt | gocov report after_success: - bash <(curl -s https://codecov.io/bash) \ No newline at end of file diff --git a/README.md b/README.md index a049f11..53d5ff2 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ ## MG Transport Library [![Build Status](https://travis-ci.org/retailcrm/mg-transport-core.svg?branch=master)](https://travis-ci.org/retailcrm/mg-transport-core) [![codecov](https://codecov.io/gh/retailcrm/mg-transport-core/branch/master/graph/badge.svg)](https://codecov.io/gh/retailcrm/mg-transport-core) -[![GoDoc](https://godoc.org/github.com/retailcrm/mg-transport-core/core?status.svg)](https://godoc.org/github.com/retailcrm/mg-transport-core/core) +[![pkg.go.dev](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white)](https://pkg.go.dev/github.com/retailcrm/mg-transport-core/core) [![Go Report Card](https://goreportcard.com/badge/github.com/retailcrm/mg-transport-core)](https://goreportcard.com/report/github.com/retailcrm/mg-transport-core) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/retailcrm/mg-transport-core/blob/master/LICENSE.md) This library provides different functions like error-reporting, logging, localization, etc. in order to make it easier to create transports. diff --git a/core/error.go b/core/error.go index 5539ad2..45118fd 100644 --- a/core/error.go +++ b/core/error.go @@ -7,6 +7,11 @@ type ErrorResponse struct { Error string `json:"error"` } +// ErrorsResponse struct +type ErrorsResponse struct { + Error []string `json:"error"` +} + // GetErrorResponse returns ErrorResponse with specified status code // Usage (with gin): // context.JSON(GetErrorResponse(http.StatusPaymentRequired, "Not enough money")) diff --git a/core/job_manager_test.go b/core/job_manager_test.go index 4300524..92ff93b 100644 --- a/core/job_manager_test.go +++ b/core/job_manager_test.go @@ -452,7 +452,7 @@ func (t *JobManagerTest) Test_RunJobOnce() { require.NotNil(t.T(), t.manager.jobs) go func() { t.runnerFlag <- false }() err := t.manager.RunJobOnce("job") - time.Sleep(200 * time.Millisecond) + time.Sleep(300 * time.Millisecond) require.NoError(t.T(), err) assert.True(t.T(), t.ranFlag()) } diff --git a/core/localizer.go b/core/localizer.go index 5901c82..084895a 100644 --- a/core/localizer.go +++ b/core/localizer.go @@ -290,7 +290,7 @@ func (l *Localizer) Localize(messageID string) (string, error) { return l.getCurrentLocalizer().Localize(&i18n.LocalizeConfig{MessageID: messageID}) } -// LocalizedTemplateMessage will return localized message with specified data, or error if message wasn't found +// LocalizeTemplateMessage will return localized message with specified data, or error if message wasn't found // It uses text/template syntax: https://golang.org/pkg/text/template/ func (l *Localizer) LocalizeTemplateMessage(messageID string, templateData map[string]interface{}) (string, error) { return l.getCurrentLocalizer().Localize(&i18n.LocalizeConfig{ @@ -313,12 +313,10 @@ func GetContextLocalizer(c *gin.Context) (*Localizer, bool) { if item, ok := c.Get(LocalizerContextKey); ok { if localizer, ok := item.(*Localizer); ok { return localizer, true - } else { - return nil, false } - } else { - return nil, false } + + return nil, false } // MustGetContextLocalizer returns Localizer instance if it exists in provided context. Panics otherwise. diff --git a/core/sentry.go b/core/sentry.go index 637251a..f9c6390 100644 --- a/core/sentry.go +++ b/core/sentry.go @@ -3,13 +3,12 @@ package core import ( "fmt" "net/http" - "path" "reflect" - "runtime" "runtime/debug" "strconv" "github.com/pkg/errors" + "github.com/retailcrm/mg-transport-core/core/stacktrace" "github.com/getsentry/raven-go" "github.com/gin-gonic/gin" @@ -39,7 +38,7 @@ type Sentry struct { DefaultError string Localizer *Localizer Logger LoggerInterface - Client *raven.Client + Client stacktrace.RavenClientInterface } // SentryTaggedStruct holds information about type, it's key in gin.Context (for middleware), and it's properties @@ -215,24 +214,31 @@ func (s *Sentry) ErrorCaptureHandler() ErrorHandlerFunc { } if recovery != nil { - stacktrace := raven.NewStacktrace(4, 3, nil) + stack := raven.NewStacktrace(4, 3, nil) recStr := fmt.Sprint(recovery) err := errors.New(recStr) go s.Client.CaptureMessageAndWait( recStr, tags, - raven.NewException(err, stacktrace), + raven.NewException(err, stack), raven.NewHttp(c.Request), ) } for _, err := range c.Errors { if s.Stacktrace { - stacktrace := newRavenStackTrace(s.Client, err.Err, 0) + stackBuilder := stacktrace.GetStackBuilderByErrorType(err.Err) + stackBuilder.SetClient(s.Client) + stack, buildErr := stackBuilder.Build().GetResult() + if buildErr != nil { + go s.Client.CaptureErrorAndWait(buildErr, tags) + stack = stacktrace.GenericStack(s.Client) + } + go s.Client.CaptureMessageAndWait( err.Error(), tags, - raven.NewException(err.Err, stacktrace), + raven.NewException(err.Err, stack), raven.NewHttp(c.Request), ) } else { @@ -381,100 +387,3 @@ func (t *SentryTaggedScalar) BuildTags(v interface{}) (items map[string]string, } return } - -// newRavenStackTrace generate stacktrace compatible with raven-go format -// It tries to extract better stacktrace from error from package "github.com/pkg/errors" -// In case of fail it will fallback to default stacktrace generation from raven-go. -// Default stacktrace highly likely will be useless, because it will not include call -// which returned error. This occurs because default stacktrace doesn't include any call -// before stacktrace generation, and raven-go will generate stacktrace here, which will end -// in trace to this file. But errors from "github.com/pkg/errors" will generate stacktrace -// immediately, it will include call which returned error, and we can fetch this trace. -// Also we can wrap default errors with error from this package, like this: -// errors.Wrap(err, err.Error) -func newRavenStackTrace(client *raven.Client, myerr error, skip int) *raven.Stacktrace { - st := getErrorStackTraceConverted(myerr, 3, client.IncludePaths()) - if st == nil { - st = raven.NewStacktrace(skip, 3, client.IncludePaths()) - } - return st -} - -// getErrorStackTraceConverted will return converted stacktrace from custom error, or nil in case of default error -func getErrorStackTraceConverted(err error, context int, appPackagePrefixes []string) *raven.Stacktrace { - st := getErrorCauseStackTrace(err) - if st == nil { - return nil - } - return convertStackTrace(st, context, appPackagePrefixes) -} - -// getErrorCauseStackTrace tries to extract stacktrace from custom error, returns nil in case of failure -func getErrorCauseStackTrace(err error) errors.StackTrace { - // This code is inspired by github.com/pkg/errors.Cause(). - var st errors.StackTrace - for err != nil { - s := getErrorStackTrace(err) - if s != nil { - st = s - } - err = getErrorCause(err) - } - return st -} - -// convertStackTrace converts github.com/pkg/errors.StackTrace to github.com/getsentry/raven-go.Stacktrace -func convertStackTrace(st errors.StackTrace, context int, appPackagePrefixes []string) *raven.Stacktrace { - // This code is borrowed from github.com/getsentry/raven-go.NewStacktrace(). - var frames []*raven.StacktraceFrame - for _, f := range st { - frame := convertFrame(f, context, appPackagePrefixes) - if frame != nil { - frames = append(frames, frame) - } - } - if len(frames) == 0 { - return nil - } - for i, j := 0, len(frames)-1; i < j; i, j = i+1, j-1 { - frames[i], frames[j] = frames[j], frames[i] - } - return &raven.Stacktrace{Frames: frames} -} - -// convertFrame converts single frame from github.com/pkg/errors.Frame to github.com/pkg/errors.Frame -func convertFrame(f errors.Frame, context int, appPackagePrefixes []string) *raven.StacktraceFrame { - // This code is borrowed from github.com/pkg/errors.Frame. - pc := uintptr(f) - 1 - fn := runtime.FuncForPC(pc) - var file string - var line int - if fn != nil { - file, line = fn.FileLine(pc) - } else { - file = "unknown" - } - return raven.NewStacktraceFrame(pc, path.Dir(file), file, line, context, appPackagePrefixes) -} - -// getErrorStackTrace will try to extract stacktrace from error using StackTrace method (default errors doesn't have it) -func getErrorStackTrace(err error) errors.StackTrace { - ster, ok := err.(interface { - StackTrace() errors.StackTrace - }) - if !ok { - return nil - } - return ster.StackTrace() -} - -// getErrorCause will try to extract original error from wrapper - it is used only if stacktrace is not present -func getErrorCause(err error) error { - cer, ok := err.(interface { - Cause() error - }) - if !ok { - return nil - } - return cer.Cause() -} diff --git a/core/sentry_test.go b/core/sentry_test.go index f0c87f3..1600f7c 100644 --- a/core/sentry_test.go +++ b/core/sentry_test.go @@ -1,31 +1,156 @@ package core import ( + "encoding/json" "errors" + "math/rand" + "net/http" + "net/http/httptest" + "sync" "testing" + "time" "github.com/getsentry/raven-go" - pkgErrors "github.com/pkg/errors" + "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" ) -type SampleStruct struct { +type sampleStruct struct { ID int Pointer *int Field string } +type ravenPacket struct { + EventID string + Message string + Tags map[string]string + Interfaces []raven.Interface +} + +func (r ravenPacket) getInterface(class string) (raven.Interface, bool) { + for _, v := range r.Interfaces { + if v.Class() == class { + return v, true + } + } + + return nil, false +} + +func (r ravenPacket) getException() (*raven.Exception, bool) { + if i, ok := r.getInterface("exception"); ok { + if r, ok := i.(*raven.Exception); ok { + return r, true + } + } + + return nil, false +} + +func (r ravenPacket) getRequest() (*raven.Http, bool) { + if i, ok := r.getInterface("request"); ok { + if r, ok := i.(*raven.Http); ok { + return r, true + } + } + + return nil, false +} + +type ravenClientMock struct { + captured []ravenPacket + mu sync.RWMutex + wg sync.WaitGroup +} + +func newRavenMock() *ravenClientMock { + rand.Seed(time.Now().UnixNano()) + return &ravenClientMock{captured: []ravenPacket{}} +} + +func (r *ravenClientMock) Reset() { + r.mu.Lock() + defer r.mu.Unlock() + r.captured = []ravenPacket{} +} + +func (r *ravenClientMock) last() (ravenPacket, error) { + r.mu.RLock() + defer r.mu.RUnlock() + + if len(r.captured) > 0 { + return r.captured[len(r.captured)-1], nil + } + + return ravenPacket{}, errors.New("empty packet list") +} + +func (r *ravenClientMock) CaptureMessageAndWait(message string, tags map[string]string, interfaces ...raven.Interface) string { + r.mu.Lock() + defer r.mu.Unlock() + defer r.wg.Done() + eventID := string(rand.Uint64()) + r.captured = append(r.captured, ravenPacket{ + EventID: eventID, + Message: message, + Tags: tags, + Interfaces: interfaces, + }) + return eventID +} + +func (r *ravenClientMock) CaptureErrorAndWait(err error, tags map[string]string, interfaces ...raven.Interface) string { + return r.CaptureMessageAndWait(err.Error(), tags, interfaces...) +} + +func (r *ravenClientMock) IncludePaths() []string { + return []string{} +} + +// simpleError is a simplest error implementation possible. The only reason why it's here is tests. +type simpleError struct { + msg string +} + +func newSimpleError(msg string) error { + return &simpleError{msg: msg} +} + +func (n *simpleError) Error() string { + return n.msg +} + +// wrappableError is a simple implementation of wrappable error +type wrappableError struct { + msg string + err error +} + +func newWrappableError(msg string, child error) error { + return &wrappableError{msg: msg, err: child} +} + +func (e *wrappableError) Error() string { + return e.msg +} + +func (e *wrappableError) Unwrap() error { + return e.err +} + type SentryTest struct { suite.Suite sentry *Sentry + gin *gin.Engine structTags *SentryTaggedStruct scalarTags *SentryTaggedScalar } func (s *SentryTest) SetupSuite() { - s.structTags = NewTaggedStruct(SampleStruct{}, "struct", map[string]string{"fake": "prop"}) + s.structTags = NewTaggedStruct(sampleStruct{}, "struct", map[string]string{"fake": "prop"}) s.scalarTags = NewTaggedScalar("", "scalar", "Scalar") require.Equal(s.T(), "struct", s.structTags.GetContextKey()) require.Equal(s.T(), "scalar", s.scalarTags.GetContextKey()) @@ -33,13 +158,16 @@ func (s *SentryTest) SetupSuite() { require.Equal(s.T(), "Scalar", s.scalarTags.GetName()) s.structTags.Tags = map[string]string{} s.sentry = NewSentry("dsn", "unknown_error", SentryTaggedTypes{}, nil, nil) + s.sentry.Client = newRavenMock() + s.gin = gin.New() + s.gin.Use(s.sentry.ErrorMiddleware()) } func (s *SentryTest) TestStruct_AddTag() { s.structTags.AddTag("test field", "Field") require.NotEmpty(s.T(), s.structTags.GetTags()) - tags, err := s.structTags.BuildTags(SampleStruct{Field: "value"}) + tags, err := s.structTags.BuildTags(sampleStruct{Field: "value"}) require.NoError(s.T(), err) require.NotEmpty(s.T(), tags) @@ -50,7 +178,7 @@ func (s *SentryTest) TestStruct_AddTag() { func (s *SentryTest) TestStruct_GetProperty() { s.structTags.AddTag("test field", "Field") - name, value, err := s.structTags.GetProperty(SampleStruct{Field: "test"}, "Field") + name, value, err := s.structTags.GetProperty(sampleStruct{Field: "test"}, "Field") require.NoError(s.T(), err) assert.Equal(s.T(), "test field", name) assert.Equal(s.T(), "test", value) @@ -71,18 +199,18 @@ func (s *SentryTest) TestStruct_GetProperty_GotScalar() { func (s *SentryTest) TestStruct_GetProperty_InvalidType() { _, _, err := s.structTags.GetProperty(Sentry{}, "Field") require.Error(s.T(), err) - assert.Equal(s.T(), "passed value should be of type `core.SampleStruct`, got `core.Sentry` instead", err.Error()) + assert.Equal(s.T(), "passed value should be of type `core.sampleStruct`, got `core.Sentry` instead", err.Error()) } func (s *SentryTest) TestStruct_GetProperty_CannotFindProperty() { - _, _, err := s.structTags.GetProperty(SampleStruct{ID: 1}, "ID") + _, _, err := s.structTags.GetProperty(sampleStruct{ID: 1}, "ID") require.Error(s.T(), err) assert.Equal(s.T(), "cannot find property `ID`", err.Error()) } func (s *SentryTest) TestStruct_GetProperty_InvalidProperty() { s.structTags.AddTag("test invalid", "Pointer") - _, _, err := s.structTags.GetProperty(SampleStruct{Pointer: nil}, "Pointer") + _, _, err := s.structTags.GetProperty(sampleStruct{Pointer: nil}, "Pointer") require.Error(s.T(), err) assert.Equal(s.T(), "invalid property, got ", err.Error()) } @@ -97,7 +225,7 @@ func (s *SentryTest) TestStruct_BuildTags_Fail() { func (s *SentryTest) TestStruct_BuildTags() { s.structTags.Tags = map[string]string{} s.structTags.AddTag("test", "Field") - tags, err := s.structTags.BuildTags(SampleStruct{Field: "value"}) + tags, err := s.structTags.BuildTags(sampleStruct{Field: "value"}) require.NoError(s.T(), err) require.NotEmpty(s.T(), tags) @@ -169,27 +297,125 @@ func (s *SentryTest) TestSentry_ErrorCaptureHandler() { assert.NotNil(s.T(), s.sentry.ErrorCaptureHandler()) } -func TestSentry_newRavenStackTrace_Fail(t *testing.T) { - defer func() { - assert.NotNil(t, recover()) - }() +func (s *SentryTest) TestSentry_CaptureRegularError() { + s.gin.GET("/test_regularError", func(c *gin.Context) { + c.Error(newSimpleError("test")) + }) - newRavenStackTrace(nil, errors.New("error"), 0) + var resp ErrorsResponse + req, err := http.NewRequest(http.MethodGet, "/test_regularError", nil) + require.NoError(s.T(), err) + + ravenMock := s.sentry.Client.(*ravenClientMock) + ravenMock.wg.Add(1) + rec := httptest.NewRecorder() + s.gin.ServeHTTP(rec, req) + require.NoError(s.T(), json.Unmarshal(rec.Body.Bytes(), &resp)) + assert.NotEmpty(s.T(), resp.Error) + assert.Equal(s.T(), s.sentry.DefaultError, resp.Error[0]) + + ravenMock.wg.Wait() + last, err := ravenMock.last() + require.NoError(s.T(), err) + assert.Equal(s.T(), "test", last.Message) + + exception, ok := last.getException() + require.True(s.T(), ok, "cannot find exception") + require.NotNil(s.T(), exception.Stacktrace) + assert.NotEmpty(s.T(), exception.Stacktrace.Frames) } -func TestSentry_newRavenStackTrace(t *testing.T) { - st := newRavenStackTrace(&raven.Client{}, errors.New("error"), 0) +// TestSentry_CaptureWrappedError is used to check if Sentry component calls stacktrace builders properly +// Actual stacktrace builder tests can be found in the corresponding package. +func (s *SentryTest) TestSentry_CaptureWrappedError() { + third := newWrappableError("third", nil) + second := newWrappableError("second", third) + first := newWrappableError("first", second) - require.NotNil(t, st) - assert.NotEmpty(t, st.Frames) + s.gin.GET("/test_wrappableError", func(c *gin.Context) { + c.Error(first) + }) + + var resp ErrorsResponse + req, err := http.NewRequest(http.MethodGet, "/test_wrappableError", nil) + require.NoError(s.T(), err) + + ravenMock := s.sentry.Client.(*ravenClientMock) + ravenMock.wg.Add(1) + rec := httptest.NewRecorder() + s.gin.ServeHTTP(rec, req) + require.NoError(s.T(), json.Unmarshal(rec.Body.Bytes(), &resp)) + assert.NotEmpty(s.T(), resp.Error) + assert.Equal(s.T(), s.sentry.DefaultError, resp.Error[0]) + + ravenMock.wg.Wait() + last, err := ravenMock.last() + require.NoError(s.T(), err) + assert.Equal(s.T(), "first", last.Message) + + exception, ok := last.getException() + require.True(s.T(), ok, "cannot find exception") + require.NotNil(s.T(), exception.Stacktrace) + assert.NotEmpty(s.T(), exception.Stacktrace.Frames) + assert.Len(s.T(), exception.Stacktrace.Frames, 3) + + // Error messages will be put into function names by parser + assert.Contains(s.T(), exception.Stacktrace.Frames[0].Function, third.Error()) + assert.Contains(s.T(), exception.Stacktrace.Frames[1].Function, second.Error()) + assert.Contains(s.T(), exception.Stacktrace.Frames[2].Function, first.Error()) } -func TestSentry_newRavenStackTrace_ErrorsPkg(t *testing.T) { - err := pkgErrors.New("error") - st := newRavenStackTrace(&raven.Client{}, err, 0) +func (s *SentryTest) TestSentry_CaptureTags() { + s.gin.GET("/test_taggedError", func(c *gin.Context) { + var intPointer = 147 + c.Set("text_tag", "text contents") + c.Set("sample_struct", sampleStruct{ + ID: 12, + Pointer: &intPointer, + Field: "field content", + }) + }, func(c *gin.Context) { + c.Error(newSimpleError("test")) + }) - require.NotNil(t, st) - assert.NotEmpty(t, st.Frames) + s.sentry.TaggedTypes = SentryTaggedTypes{ + NewTaggedScalar("", "text_tag", "TextTag"), + NewTaggedStruct(sampleStruct{}, "sample_struct", map[string]string{ + "id": "ID", + "pointer": "Pointer", + "field item": "Field", + }), + } + + var resp ErrorsResponse + req, err := http.NewRequest(http.MethodGet, "/test_taggedError", nil) + require.NoError(s.T(), err) + + ravenMock := s.sentry.Client.(*ravenClientMock) + ravenMock.wg.Add(1) + rec := httptest.NewRecorder() + s.gin.ServeHTTP(rec, req) + require.NoError(s.T(), json.Unmarshal(rec.Body.Bytes(), &resp)) + assert.NotEmpty(s.T(), resp.Error) + assert.Equal(s.T(), s.sentry.DefaultError, resp.Error[0]) + + ravenMock.wg.Wait() + last, err := ravenMock.last() + require.NoError(s.T(), err) + assert.Equal(s.T(), "test", last.Message) + + exception, ok := last.getException() + require.True(s.T(), ok, "cannot find exception") + require.NotNil(s.T(), exception.Stacktrace) + assert.NotEmpty(s.T(), exception.Stacktrace.Frames) + + // endpoint tag is present by default + require.NotEmpty(s.T(), last.Tags) + assert.True(s.T(), len(last.Tags) == 5) + assert.Equal(s.T(), "text contents", last.Tags["TextTag"]) + assert.Equal(s.T(), "12", last.Tags["id"]) + assert.Equal(s.T(), "147", last.Tags["pointer"]) + assert.Equal(s.T(), "field content", last.Tags["field item"]) } func TestSentry_Suite(t *testing.T) { diff --git a/core/stacktrace/abstract_stack_builder.go b/core/stacktrace/abstract_stack_builder.go new file mode 100644 index 0000000..28e4363 --- /dev/null +++ b/core/stacktrace/abstract_stack_builder.go @@ -0,0 +1,52 @@ +package stacktrace + +import ( + "github.com/getsentry/raven-go" + "github.com/pkg/errors" +) + +// ErrUnfeasibleBuilder will be returned if builder for stacktrace was chosen incorrectly +var ErrUnfeasibleBuilder = errors.New("unfeasible builder for this error type") + +// StackBuilderInterface is an interface for every stacktrace builder +type StackBuilderInterface interface { + SetClient(RavenClientInterface) StackBuilderInterface + SetError(error) StackBuilderInterface + Build() StackBuilderInterface + GetResult() (*raven.Stacktrace, error) +} + +// AbstractStackBuilder contains methods, which would be implemented in every builder anyway +type AbstractStackBuilder struct { + err error + buildErr error + client RavenClientInterface + stack *raven.Stacktrace +} + +// SetClient sets *raven.Client into builder. RavenClientInterface is used, so, any client might be used via facade. +func (a *AbstractStackBuilder) SetClient(client RavenClientInterface) StackBuilderInterface { + a.client = client + return a +} + +// SetError sets error in builder, which will be processed +func (a *AbstractStackBuilder) SetError(err error) StackBuilderInterface { + a.err = err + return a +} + +// Build stacktrace. Only implemented in the children. +func (a *AbstractStackBuilder) Build() StackBuilderInterface { + panic("not implemented") +} + +// GetResult returns builder result. +func (a *AbstractStackBuilder) GetResult() (*raven.Stacktrace, error) { + return a.stack, a.buildErr +} + +// FallbackToGeneric fallbacks to GenericStackBuilder method +func (a *AbstractStackBuilder) FallbackToGeneric() { + a.stack, a.err = GenericStack(a.client), nil +} diff --git a/core/stacktrace/abstract_stack_builder_test.go b/core/stacktrace/abstract_stack_builder_test.go new file mode 100644 index 0000000..69b775a --- /dev/null +++ b/core/stacktrace/abstract_stack_builder_test.go @@ -0,0 +1,60 @@ +package stacktrace + +import ( + "errors" + "testing" + + "github.com/getsentry/raven-go" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" +) + +type AbstractStackBuilderSuite struct { + builder *AbstractStackBuilder + suite.Suite +} + +func TestAbstractStackBuilder(t *testing.T) { + suite.Run(t, new(AbstractStackBuilderSuite)) +} + +func (s *AbstractStackBuilderSuite) SetupSuite() { + s.builder = &AbstractStackBuilder{} +} + +func (s *AbstractStackBuilderSuite) Test_SetClient() { + require.Nil(s.T(), s.builder.client) + client, _ := raven.New("fake dsn") + s.builder.SetClient(client) + assert.NotNil(s.T(), s.builder.client) +} + +func (s *AbstractStackBuilderSuite) Test_SetError() { + require.Nil(s.T(), s.builder.err) + s.builder.SetError(errors.New("test err")) + assert.NotNil(s.T(), s.builder.err) +} + +func (s *AbstractStackBuilderSuite) Test_Build() { + defer func() { + r := recover() + require.NotNil(s.T(), r) + require.IsType(s.T(), "", r) + assert.Equal(s.T(), "not implemented", r.(string)) + }() + + s.builder.Build() +} + +func (s *AbstractStackBuilderSuite) Test_GetResult() { + buildErr := errors.New("build err") + stack := raven.NewStacktrace(0, 3, []string{}) + s.builder.buildErr = buildErr + s.builder.stack = stack + resultStack, resultErr := s.builder.GetResult() + + assert.Error(s.T(), resultErr) + assert.Equal(s.T(), buildErr, resultErr) + assert.Equal(s.T(), *stack, *resultStack) +} diff --git a/core/stacktrace/generic_stack_builder.go b/core/stacktrace/generic_stack_builder.go new file mode 100644 index 0000000..3d9e251 --- /dev/null +++ b/core/stacktrace/generic_stack_builder.go @@ -0,0 +1,21 @@ +package stacktrace + +import ( + "github.com/getsentry/raven-go" +) + +// GenericStackBuilder uses raven.NewStacktrace to build stacktrace. Only client is needed here. +type GenericStackBuilder struct { + AbstractStackBuilder +} + +// Build returns generic stacktrace. +func (b *GenericStackBuilder) Build() StackBuilderInterface { + b.stack = GenericStack(b.client) + return b +} + +// GenericStack returns generic stacktrace. +func GenericStack(client RavenClientInterface) *raven.Stacktrace { + return raven.NewStacktrace(0, 3, client.IncludePaths()) +} diff --git a/core/stacktrace/generic_stack_builder_test.go b/core/stacktrace/generic_stack_builder_test.go new file mode 100644 index 0000000..10a445a --- /dev/null +++ b/core/stacktrace/generic_stack_builder_test.go @@ -0,0 +1,32 @@ +package stacktrace + +import ( + "testing" + + "github.com/getsentry/raven-go" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" +) + +type GenericStackBuilderSuite struct { + builder *GenericStackBuilder + suite.Suite +} + +func TestGenericStack(t *testing.T) { + suite.Run(t, new(GenericStackBuilderSuite)) +} + +func (s *GenericStackBuilderSuite) SetupSuite() { + client, _ := raven.New("fake dsn") + s.builder = &GenericStackBuilder{AbstractStackBuilder{ + client: client, + }} +} + +func (s *GenericStackBuilderSuite) Test_Build() { + stack, err := s.builder.Build().GetResult() + require.Nil(s.T(), err) + assert.NotEmpty(s.T(), stack) +} diff --git a/core/stacktrace/pkg_errors_builder.go b/core/stacktrace/pkg_errors_builder.go new file mode 100644 index 0000000..3c3df5b --- /dev/null +++ b/core/stacktrace/pkg_errors_builder.go @@ -0,0 +1,88 @@ +package stacktrace + +import ( + pkgErrors "github.com/pkg/errors" +) + +// PkgErrorCauseable is an interface for checking Cause() method existence in the error +type PkgErrorCauseable interface { + Cause() error +} + +// PkgErrorTraceable is an interface for checking StackTrace() method existence in the error +type PkgErrorTraceable interface { + StackTrace() pkgErrors.StackTrace +} + +// PkgErrorsStackTransformer transforms stack data from github.com/pkg/errors error to stacktrace.Stacktrace +type PkgErrorsStackTransformer struct { + stack pkgErrors.StackTrace +} + +// NewPkgErrorsStackTransformer is a PkgErrorsStackTransformer constructor +func NewPkgErrorsStackTransformer(stack pkgErrors.StackTrace) *PkgErrorsStackTransformer { + return &PkgErrorsStackTransformer{stack: stack} +} + +// Stack returns stacktrace (which is []uintptr internally, each uintptc is a pc) +func (p *PkgErrorsStackTransformer) Stack() Stacktrace { + if p.stack == nil { + return Stacktrace{} + } + + result := make(Stacktrace, len(p.stack)) + for i, frame := range p.stack { + result[i] = Frame(uintptr(frame) - 1) + } + return result +} + +// PkgErrorsBuilder builds stacktrace with data from github.com/pkg/errors error +type PkgErrorsBuilder struct { + AbstractStackBuilder +} + +// Build stacktrace +func (b *PkgErrorsBuilder) Build() StackBuilderInterface { + if !isPkgErrors(b.err) { + b.buildErr = ErrUnfeasibleBuilder + return b + } + + var stack pkgErrors.StackTrace + err := b.err + + for err != nil { + s := b.getErrorStack(err) + if s != nil { + stack = s + } + err = b.getErrorCause(err) + } + + if len(stack) > 0 { + b.stack = NewRavenStacktraceBuilder(NewPkgErrorsStackTransformer(stack)).Build(3, b.client.IncludePaths()) + } else { + b.buildErr = ErrUnfeasibleBuilder + } + + return b +} + +// getErrorCause will try to extract original error from wrapper - it is used only if stacktrace is not present +func (b *PkgErrorsBuilder) getErrorCause(err error) error { + causeable, ok := err.(PkgErrorCauseable) + if !ok { + return nil + } + return causeable.Cause() +} + +// getErrorStackTrace will try to extract stacktrace from error using StackTrace method (default errors doesn't have it) +func (b *PkgErrorsBuilder) getErrorStack(err error) pkgErrors.StackTrace { + traceable, ok := err.(PkgErrorTraceable) + if !ok { + return nil + } + return traceable.StackTrace() +} diff --git a/core/stacktrace/pkg_errors_builder_test.go b/core/stacktrace/pkg_errors_builder_test.go new file mode 100644 index 0000000..3981c05 --- /dev/null +++ b/core/stacktrace/pkg_errors_builder_test.go @@ -0,0 +1,101 @@ +package stacktrace + +import ( + "errors" + "testing" + + "github.com/getsentry/raven-go" + pkgErrors "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" +) + +// errorWithCause has Cause() method, but doesn't have StackTrace() method +type errorWithCause struct { + msg string + cause error +} + +func newErrorWithCause(msg string, cause error) error { + return &errorWithCause{ + msg: msg, + cause: cause, + } +} + +func (e *errorWithCause) Error() string { + return e.msg +} + +func (e *errorWithCause) Cause() error { + return e.cause +} + +type PkgErrorsStackProviderSuite struct { + transformer *PkgErrorsStackTransformer + suite.Suite +} + +func (s *PkgErrorsStackProviderSuite) SetupSuite() { + s.transformer = &PkgErrorsStackTransformer{} +} + +func (s *PkgErrorsStackProviderSuite) Test_Nil() { + s.transformer.stack = nil + assert.Empty(s.T(), s.transformer.Stack()) +} + +func (s *PkgErrorsStackProviderSuite) Test_Empty() { + s.transformer.stack = pkgErrors.StackTrace{} + assert.Empty(s.T(), s.transformer.Stack()) +} + +func (s *PkgErrorsStackProviderSuite) Test_Full() { + testErr := pkgErrors.New("test") + s.transformer.stack = testErr.(PkgErrorTraceable).StackTrace() + assert.NotEmpty(s.T(), s.transformer.Stack()) +} + +type PkgErrorsBuilderSuite struct { + builder *PkgErrorsBuilder + suite.Suite +} + +func (s *PkgErrorsBuilderSuite) SetupTest() { + s.builder = &PkgErrorsBuilder{} + client, _ := raven.New("fake dsn") + s.builder.SetClient(client) +} + +func (s *PkgErrorsBuilderSuite) Test_Stackless() { + s.builder.SetError(errors.New("simple")) + stack, err := s.builder.Build().GetResult() + require.Error(s.T(), err) + assert.Equal(s.T(), ErrUnfeasibleBuilder, err) + assert.Empty(s.T(), stack) +} + +func (s *PkgErrorsBuilderSuite) Test_WithStack() { + s.builder.SetError(pkgErrors.New("with stack")) + stack, err := s.builder.Build().GetResult() + require.NoError(s.T(), err) + require.NotEmpty(s.T(), stack) + assert.NotEmpty(s.T(), stack.Frames) +} + +func (s *PkgErrorsBuilderSuite) Test_CauseWithStack() { + s.builder.SetError(newErrorWithCause("cause with stack", pkgErrors.New("with stack"))) + stack, err := s.builder.Build().GetResult() + require.NoError(s.T(), err) + require.NotEmpty(s.T(), stack) + assert.NotEmpty(s.T(), stack.Frames) +} + +func TestPkgErrorsStackProvider(t *testing.T) { + suite.Run(t, new(PkgErrorsStackProviderSuite)) +} + +func TestPkgErrorsBuilder(t *testing.T) { + suite.Run(t, new(PkgErrorsBuilderSuite)) +} diff --git a/core/stacktrace/raven_client_interface.go b/core/stacktrace/raven_client_interface.go new file mode 100644 index 0000000..d3c916d --- /dev/null +++ b/core/stacktrace/raven_client_interface.go @@ -0,0 +1,10 @@ +package stacktrace + +import "github.com/getsentry/raven-go" + +// RavenClientInterface includes all necessary calls from *raven.Client. Therefore, it can be mocked or replaced. +type RavenClientInterface interface { + CaptureMessageAndWait(message string, tags map[string]string, interfaces ...raven.Interface) string + CaptureErrorAndWait(err error, tags map[string]string, interfaces ...raven.Interface) string + IncludePaths() []string +} diff --git a/core/stacktrace/raven_stacktrace_builder.go b/core/stacktrace/raven_stacktrace_builder.go new file mode 100644 index 0000000..7a25e1c --- /dev/null +++ b/core/stacktrace/raven_stacktrace_builder.go @@ -0,0 +1,69 @@ +package stacktrace + +import ( + "path" + "runtime" + + "github.com/getsentry/raven-go" +) + +// Frame is a program counter inside a stack frame. +type Frame uintptr + +// Stacktrace is stack of Frames +type Stacktrace []Frame + +// RavenStackTransformer is an interface for any component, which will transform some unknown stacktrace data to stacktrace.Stacktrace +type RavenStackTransformer interface { + Stack() Stacktrace +} + +// RavenStacktraceBuilder builds *raven.Stacktrace for any generic stack data +type RavenStacktraceBuilder struct { + transformer RavenStackTransformer +} + +// NewRavenStacktraceBuilder is a RavenStacktraceBuilder constructor +func NewRavenStacktraceBuilder(p RavenStackTransformer) *RavenStacktraceBuilder { + return (&RavenStacktraceBuilder{}).SetTransformer(p) +} + +// SetTransformer sets stack transformer into stacktrace builder +func (b *RavenStacktraceBuilder) SetTransformer(p RavenStackTransformer) *RavenStacktraceBuilder { + b.transformer = p + return b +} + +// Build converts generic stacktrace to to github.com/getsentry/raven-go.Stacktrace +func (b *RavenStacktraceBuilder) Build(context int, appPackagePrefixes []string) *raven.Stacktrace { + // This code is borrowed from github.com/getsentry/raven-go.NewStacktrace(). + var frames []*raven.StacktraceFrame + for _, f := range b.transformer.Stack() { + frame := b.convertFrame(f, context, appPackagePrefixes) + if frame != nil { + frames = append(frames, frame) + } + } + if len(frames) == 0 { + return nil + } + for i, j := 0, len(frames)-1; i < j; i, j = i+1, j-1 { + frames[i], frames[j] = frames[j], frames[i] + } + return &raven.Stacktrace{Frames: frames} +} + +// convertFrame converts single generic stacktrace frame to github.com/pkg/errors.Frame +func (b *RavenStacktraceBuilder) convertFrame(f Frame, context int, appPackagePrefixes []string) *raven.StacktraceFrame { + // This code is borrowed from github.com/pkg/errors.Frame. + pc := uintptr(f) - 1 + fn := runtime.FuncForPC(pc) + var file string + var line int + if fn != nil { + file, line = fn.FileLine(pc) + } else { + file = "unknown" + } + return raven.NewStacktraceFrame(pc, path.Dir(file), file, line, context, appPackagePrefixes) +} diff --git a/core/stacktrace/raven_stacktrace_builder_test.go b/core/stacktrace/raven_stacktrace_builder_test.go new file mode 100644 index 0000000..a4a0fee --- /dev/null +++ b/core/stacktrace/raven_stacktrace_builder_test.go @@ -0,0 +1,55 @@ +package stacktrace + +import ( + "runtime" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" +) + +type ravenMockTransformer struct { + mock.Mock +} + +func (r *ravenMockTransformer) Stack() Stacktrace { + args := r.Called() + return args.Get(0).(Stacktrace) +} + +type RavenStacktraceBuilderSuite struct { + suite.Suite +} + +func (s *RavenStacktraceBuilderSuite) callers() Stacktrace { + const depth = 32 + var pcs [depth]uintptr + n := runtime.Callers(3, pcs[:]) + st := make(Stacktrace, n) + for i := 0; i < n; i++ { + st[i] = Frame(pcs[i]) + } + return st +} + +func (s *RavenStacktraceBuilderSuite) Test_BuildEmpty() { + testTransformer := new(ravenMockTransformer) + testTransformer.On("Stack", mock.Anything).Return(Stacktrace{}) + + assert.Nil(s.T(), NewRavenStacktraceBuilder(testTransformer).Build(3, []string{})) +} + +func (s *RavenStacktraceBuilderSuite) Test_BuildActual() { + testTransformer := new(ravenMockTransformer) + testTransformer.On("Stack", mock.Anything).Return(s.callers()) + stack := NewRavenStacktraceBuilder(testTransformer).Build(3, []string{}) + + require.NotNil(s.T(), stack) + assert.NotEmpty(s.T(), stack.Frames) +} + +func TestRavenStacktraceBuilder(t *testing.T) { + suite.Run(t, new(RavenStacktraceBuilderSuite)) +} diff --git a/core/stacktrace/stack_builder_factory.go b/core/stacktrace/stack_builder_factory.go new file mode 100644 index 0000000..ce9dae2 --- /dev/null +++ b/core/stacktrace/stack_builder_factory.go @@ -0,0 +1,22 @@ +package stacktrace + +// GetStackBuilderByErrorType tries to guess which stacktrace builder would be feasible for passed error. +// For example, errors from github.com/pkg/errors have StackTrace() method, and Go 1.13 errors can be unwrapped. +func GetStackBuilderByErrorType(err error) StackBuilderInterface { + if isPkgErrors(err) { + return &PkgErrorsBuilder{AbstractStackBuilder{err: err}} + } + + if _, ok := err.(Unwrappable); ok { + return &UnwrapBuilder{AbstractStackBuilder{err: err}} + } + + return &GenericStackBuilder{AbstractStackBuilder{err: err}} +} + +// isPkgErrors returns true if passed error might be github.com/pkg/errors error +func isPkgErrors(err error) bool { + _, okTraceable := err.(PkgErrorTraceable) + _, okCauseable := err.(PkgErrorCauseable) + return okTraceable || okCauseable +} diff --git a/core/stacktrace/stack_builder_factory_test.go b/core/stacktrace/stack_builder_factory_test.go new file mode 100644 index 0000000..4edc3c3 --- /dev/null +++ b/core/stacktrace/stack_builder_factory_test.go @@ -0,0 +1,27 @@ +package stacktrace + +import ( + "errors" + "testing" + + pkgErrors "github.com/pkg/errors" + "github.com/stretchr/testify/assert" +) + +func TestGetStackBuilderByErrorType_PkgErrors(t *testing.T) { + testErr := pkgErrors.New("pkg/errors err") + builder := GetStackBuilderByErrorType(testErr) + assert.IsType(t, &PkgErrorsBuilder{}, builder) +} + +func TestGetStackBuilderByErrorType_UnwrapBuilder(t *testing.T) { + testErr := newWrappableError("first", newWrappableError("second", errors.New("third"))) + builder := GetStackBuilderByErrorType(testErr) + assert.IsType(t, &UnwrapBuilder{}, builder) +} + +func TestGetStackBuilderByErrorType_Generic(t *testing.T) { + defaultErr := errors.New("default err") + builder := GetStackBuilderByErrorType(defaultErr) + assert.IsType(t, &GenericStackBuilder{}, builder) +} diff --git a/core/stacktrace/unwrap_builder.go b/core/stacktrace/unwrap_builder.go new file mode 100644 index 0000000..3e1d531 --- /dev/null +++ b/core/stacktrace/unwrap_builder.go @@ -0,0 +1,56 @@ +package stacktrace + +import ( + "github.com/getsentry/raven-go" +) + +// Unwrappable is the interface for errors with Unwrap() method +type Unwrappable interface { + Unwrap() error +} + +// UnwrapBuilder builds stacktrace from the chain of wrapped errors +type UnwrapBuilder struct { + AbstractStackBuilder +} + +// Build stacktrace +func (b *UnwrapBuilder) Build() StackBuilderInterface { + if _, ok := b.err.(Unwrappable); !ok { + b.buildErr = ErrUnfeasibleBuilder + return b + } + + err := b.err + frames := []*raven.StacktraceFrame{} + + for err != nil { + frames = append(frames, raven.NewStacktraceFrame( + 0, + ": "+err.Error(), + "", + 0, + 3, + b.client.IncludePaths(), + )) + + if item, ok := err.(Unwrappable); ok { + err = item.Unwrap() + } else { + err = nil + } + } + + if len(frames) <= 1 { + b.buildErr = ErrUnfeasibleBuilder + return b + } + + // Sentry wants the frames with the oldest first, so reverse them + for i, j := 0, len(frames)-1; i < j; i, j = i+1, j-1 { + frames[i], frames[j] = frames[j], frames[i] + } + + b.stack = &raven.Stacktrace{Frames: frames} + return b +} diff --git a/core/stacktrace/unwrap_builder_test.go b/core/stacktrace/unwrap_builder_test.go new file mode 100644 index 0000000..aa7af38 --- /dev/null +++ b/core/stacktrace/unwrap_builder_test.go @@ -0,0 +1,88 @@ +package stacktrace + +import ( + "errors" + "testing" + + "github.com/getsentry/raven-go" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" +) + +// simpleError is a simplest error implementation possible. The only reason why it's here is tests. +type simpleError struct { + msg string +} + +func newSimpleError(msg string) error { + return &simpleError{msg: msg} +} + +func (n *simpleError) Error() string { + return n.msg +} + +// wrappableError is a simple implementation of wrappable error +type wrappableError struct { + msg string + err error +} + +func newWrappableError(msg string, child error) error { + return &wrappableError{msg: msg, err: child} +} + +func (e *wrappableError) Error() string { + return e.msg +} + +func (e *wrappableError) Unwrap() error { + return e.err +} + +type UnwrapBuilderSuite struct { + builder *UnwrapBuilder + suite.Suite +} + +func TestUnwrapBuilder(t *testing.T) { + suite.Run(t, new(UnwrapBuilderSuite)) +} + +func (s *UnwrapBuilderSuite) SetupTest() { + client, _ := raven.New("fake dsn") + s.builder = &UnwrapBuilder{} + s.builder.SetClient(client) +} + +func (s *UnwrapBuilderSuite) TestBuild_Nil() { + stack, err := s.builder.Build().GetResult() + require.Error(s.T(), err) + if stack != nil { + assert.Empty(s.T(), stack.Frames) + } + assert.Equal(s.T(), ErrUnfeasibleBuilder, err) +} + +func (s *UnwrapBuilderSuite) TestBuild_NoUnwrap() { + s.builder.SetError(newSimpleError("fake")) + stack, buildErr := s.builder.Build().GetResult() + require.Error(s.T(), buildErr) + require.Equal(s.T(), ErrUnfeasibleBuilder, buildErr) + assert.Empty(s.T(), stack) +} + +func (s *UnwrapBuilderSuite) TestBuild_WrappableHasWrapped() { + testErr := newWrappableError("first", newWrappableError("second", errors.New("third"))) + _, ok := testErr.(Unwrappable) + require.True(s.T(), ok) + + s.builder.SetError(testErr) + stack, buildErr := s.builder.Build().GetResult() + require.NoError(s.T(), buildErr) + require.NotNil(s.T(), stack) + require.NotNil(s.T(), stack.Frames) + assert.NotEmpty(s.T(), stack.Frames) + assert.True(s.T(), len(stack.Frames) > 1) +} diff --git a/go.mod b/go.mod index 0013a60..c684ae2 100644 --- a/go.mod +++ b/go.mod @@ -33,7 +33,7 @@ require ( golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 // indirect golang.org/x/sys v0.0.0-20200428200454-593003d681fa // indirect golang.org/x/text v0.3.2 - gopkg.in/go-playground/validator.v9 v9.30.2 // indirect + golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 gopkg.in/gormigrate.v1 v1.6.0 gopkg.in/yaml.v2 v2.2.8 ) diff --git a/go.sum b/go.sum index a26b9a7..02faec3 100644 --- a/go.sum +++ b/go.sum @@ -48,20 +48,14 @@ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.4.0 h1:3tMoCCfM7ppqsR0ptz/wi1impNpT7/9wQtMZ8lr1mCQ= github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM= -github.com/gin-gonic/gin v1.5.0 h1:fi+bqFAx/oLK54somfCtEZs9HeH1LHVoEPUgARpTqyc= -github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do= github.com/gin-gonic/gin v1.6.2 h1:88crIK23zO6TqlQBt+f9FrPJNKm9ZEr7qjp9vl/d5TM= github.com/gin-gonic/gin v1.6.2/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.12.1 h1:2FITxuFt/xuCNP1Acdhv62OzaCiviiE4kotfhkmOqEc= -github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= -github.com/go-playground/universal-translator v0.16.0 h1:X++omBR/4cE2MNg91AoC3rmGrCjJ8eAeUP/K/EKx4DM= -github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= @@ -87,8 +81,6 @@ github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfb github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= @@ -103,6 +95,7 @@ github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= @@ -139,10 +132,6 @@ github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.7 h1:KfgG9LzI+pYjr4xvmz/5H4FXjokeP+rlHLhv3iH62Fo= -github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.8 h1:QiWkFLKq0T7mpzwOTu6BzNDbfTE8OLrYhVKYMLF46Ok= -github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= @@ -157,8 +146,6 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/leodido/go-urn v1.1.0 h1:Sm1gr51B1kKyfD2BlRcLSiEkffoG96g6TPv6eRoEiB8= -github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= @@ -167,9 +154,6 @@ github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= -github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM= -github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= @@ -298,10 +282,6 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191210023423-ac6580df4449 h1:gSbV7h1NRL2G1xTg/owz62CST1oJBmxy4QpMMregXVQ= -golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200428200454-593003d681fa h1:yMbJOvnfYkO1dSAviTu/ZguZWLBTXx4xE3LYrxUCCiA= @@ -321,6 +301,7 @@ golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191004055002-72853e10c5a3/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= @@ -348,18 +329,12 @@ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMy gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= -gopkg.in/go-playground/validator.v9 v9.29.1 h1:SvGtYmN60a5CVKTOzMSyfzWDeZRxRuGvRQyEAKbw1xc= -gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= -gopkg.in/go-playground/validator.v9 v9.30.2 h1:icxYLlYflpazIV3ufMoNB9h9SYMQ37DZ8CTwkU4pnOs= -gopkg.in/go-playground/validator.v9 v9.30.2/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= gopkg.in/gormigrate.v1 v1.6.0 h1:XpYM6RHQPmzwY7Uyu+t+xxMXc86JYFJn4nEc9HzQjsI= gopkg.in/gormigrate.v1 v1.6.0/go.mod h1:Lf00lQrHqfSYWiTtPcyQabsDdM6ejZaMgV0OU6JMSlw= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo= -gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=