Better stacktrace generation system, better tests for Sentry, new badge

This commit is contained in:
Pavel 2020-08-03 14:45:16 +03:00 committed by GitHub
parent e6dd1643cf
commit 1de2e5626f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 959 additions and 162 deletions

View File

@ -4,9 +4,12 @@ env:
go: go:
- '1.12' - '1.12'
- '1.13' - '1.13'
- '1.14'
before_install: before_install:
- go mod tidy - go mod tidy
script: script:
- go test ./... -v -cpu 2 -timeout 2m -race -cover -coverprofile=coverage.txt -covermode=atomic - 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: after_success:
- bash <(curl -s https://codecov.io/bash) - bash <(curl -s https://codecov.io/bash)

View File

@ -1,7 +1,7 @@
## MG Transport Library ## MG Transport Library
[![Build Status](https://travis-ci.org/retailcrm/mg-transport-core.svg?branch=master)](https://travis-ci.org/retailcrm/mg-transport-core) [![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) [![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) [![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) [![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. This library provides different functions like error-reporting, logging, localization, etc. in order to make it easier to create transports.

View File

@ -7,6 +7,11 @@ type ErrorResponse struct {
Error string `json:"error"` Error string `json:"error"`
} }
// ErrorsResponse struct
type ErrorsResponse struct {
Error []string `json:"error"`
}
// GetErrorResponse returns ErrorResponse with specified status code // GetErrorResponse returns ErrorResponse with specified status code
// Usage (with gin): // Usage (with gin):
// context.JSON(GetErrorResponse(http.StatusPaymentRequired, "Not enough money")) // context.JSON(GetErrorResponse(http.StatusPaymentRequired, "Not enough money"))

View File

@ -452,7 +452,7 @@ func (t *JobManagerTest) Test_RunJobOnce() {
require.NotNil(t.T(), t.manager.jobs) require.NotNil(t.T(), t.manager.jobs)
go func() { t.runnerFlag <- false }() go func() { t.runnerFlag <- false }()
err := t.manager.RunJobOnce("job") err := t.manager.RunJobOnce("job")
time.Sleep(200 * time.Millisecond) time.Sleep(300 * time.Millisecond)
require.NoError(t.T(), err) require.NoError(t.T(), err)
assert.True(t.T(), t.ranFlag()) assert.True(t.T(), t.ranFlag())
} }

View File

@ -290,7 +290,7 @@ func (l *Localizer) Localize(messageID string) (string, error) {
return l.getCurrentLocalizer().Localize(&i18n.LocalizeConfig{MessageID: messageID}) 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/ // It uses text/template syntax: https://golang.org/pkg/text/template/
func (l *Localizer) LocalizeTemplateMessage(messageID string, templateData map[string]interface{}) (string, error) { func (l *Localizer) LocalizeTemplateMessage(messageID string, templateData map[string]interface{}) (string, error) {
return l.getCurrentLocalizer().Localize(&i18n.LocalizeConfig{ 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 item, ok := c.Get(LocalizerContextKey); ok {
if localizer, ok := item.(*Localizer); ok { if localizer, ok := item.(*Localizer); ok {
return localizer, true 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. // MustGetContextLocalizer returns Localizer instance if it exists in provided context. Panics otherwise.

View File

@ -3,13 +3,12 @@ package core
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"path"
"reflect" "reflect"
"runtime"
"runtime/debug" "runtime/debug"
"strconv" "strconv"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/retailcrm/mg-transport-core/core/stacktrace"
"github.com/getsentry/raven-go" "github.com/getsentry/raven-go"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@ -39,7 +38,7 @@ type Sentry struct {
DefaultError string DefaultError string
Localizer *Localizer Localizer *Localizer
Logger LoggerInterface 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 // 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 { if recovery != nil {
stacktrace := raven.NewStacktrace(4, 3, nil) stack := raven.NewStacktrace(4, 3, nil)
recStr := fmt.Sprint(recovery) recStr := fmt.Sprint(recovery)
err := errors.New(recStr) err := errors.New(recStr)
go s.Client.CaptureMessageAndWait( go s.Client.CaptureMessageAndWait(
recStr, recStr,
tags, tags,
raven.NewException(err, stacktrace), raven.NewException(err, stack),
raven.NewHttp(c.Request), raven.NewHttp(c.Request),
) )
} }
for _, err := range c.Errors { for _, err := range c.Errors {
if s.Stacktrace { 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( go s.Client.CaptureMessageAndWait(
err.Error(), err.Error(),
tags, tags,
raven.NewException(err.Err, stacktrace), raven.NewException(err.Err, stack),
raven.NewHttp(c.Request), raven.NewHttp(c.Request),
) )
} else { } else {
@ -381,100 +387,3 @@ func (t *SentryTaggedScalar) BuildTags(v interface{}) (items map[string]string,
} }
return 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()
}

View File

@ -1,31 +1,156 @@
package core package core
import ( import (
"encoding/json"
"errors" "errors"
"math/rand"
"net/http"
"net/http/httptest"
"sync"
"testing" "testing"
"time"
"github.com/getsentry/raven-go" "github.com/getsentry/raven-go"
pkgErrors "github.com/pkg/errors" "github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
) )
type SampleStruct struct { type sampleStruct struct {
ID int ID int
Pointer *int Pointer *int
Field string 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 { type SentryTest struct {
suite.Suite suite.Suite
sentry *Sentry sentry *Sentry
gin *gin.Engine
structTags *SentryTaggedStruct structTags *SentryTaggedStruct
scalarTags *SentryTaggedScalar scalarTags *SentryTaggedScalar
} }
func (s *SentryTest) SetupSuite() { 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") s.scalarTags = NewTaggedScalar("", "scalar", "Scalar")
require.Equal(s.T(), "struct", s.structTags.GetContextKey()) require.Equal(s.T(), "struct", s.structTags.GetContextKey())
require.Equal(s.T(), "scalar", s.scalarTags.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()) require.Equal(s.T(), "Scalar", s.scalarTags.GetName())
s.structTags.Tags = map[string]string{} s.structTags.Tags = map[string]string{}
s.sentry = NewSentry("dsn", "unknown_error", SentryTaggedTypes{}, nil, nil) 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() { func (s *SentryTest) TestStruct_AddTag() {
s.structTags.AddTag("test field", "Field") s.structTags.AddTag("test field", "Field")
require.NotEmpty(s.T(), s.structTags.GetTags()) 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.NoError(s.T(), err)
require.NotEmpty(s.T(), tags) require.NotEmpty(s.T(), tags)
@ -50,7 +178,7 @@ func (s *SentryTest) TestStruct_AddTag() {
func (s *SentryTest) TestStruct_GetProperty() { func (s *SentryTest) TestStruct_GetProperty() {
s.structTags.AddTag("test field", "Field") 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) require.NoError(s.T(), err)
assert.Equal(s.T(), "test field", name) assert.Equal(s.T(), "test field", name)
assert.Equal(s.T(), "test", value) assert.Equal(s.T(), "test", value)
@ -71,18 +199,18 @@ func (s *SentryTest) TestStruct_GetProperty_GotScalar() {
func (s *SentryTest) TestStruct_GetProperty_InvalidType() { func (s *SentryTest) TestStruct_GetProperty_InvalidType() {
_, _, err := s.structTags.GetProperty(Sentry{}, "Field") _, _, err := s.structTags.GetProperty(Sentry{}, "Field")
require.Error(s.T(), err) 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() { 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) require.Error(s.T(), err)
assert.Equal(s.T(), "cannot find property `ID`", err.Error()) assert.Equal(s.T(), "cannot find property `ID`", err.Error())
} }
func (s *SentryTest) TestStruct_GetProperty_InvalidProperty() { func (s *SentryTest) TestStruct_GetProperty_InvalidProperty() {
s.structTags.AddTag("test invalid", "Pointer") 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) require.Error(s.T(), err)
assert.Equal(s.T(), "invalid property, got <invalid Value>", err.Error()) assert.Equal(s.T(), "invalid property, got <invalid Value>", err.Error())
} }
@ -97,7 +225,7 @@ func (s *SentryTest) TestStruct_BuildTags_Fail() {
func (s *SentryTest) TestStruct_BuildTags() { func (s *SentryTest) TestStruct_BuildTags() {
s.structTags.Tags = map[string]string{} s.structTags.Tags = map[string]string{}
s.structTags.AddTag("test", "Field") 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.NoError(s.T(), err)
require.NotEmpty(s.T(), tags) require.NotEmpty(s.T(), tags)
@ -169,27 +297,125 @@ func (s *SentryTest) TestSentry_ErrorCaptureHandler() {
assert.NotNil(s.T(), s.sentry.ErrorCaptureHandler()) assert.NotNil(s.T(), s.sentry.ErrorCaptureHandler())
} }
func TestSentry_newRavenStackTrace_Fail(t *testing.T) { func (s *SentryTest) TestSentry_CaptureRegularError() {
defer func() { s.gin.GET("/test_regularError", func(c *gin.Context) {
assert.NotNil(t, recover()) 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) { // TestSentry_CaptureWrappedError is used to check if Sentry component calls stacktrace builders properly
st := newRavenStackTrace(&raven.Client{}, errors.New("error"), 0) // 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) s.gin.GET("/test_wrappableError", func(c *gin.Context) {
assert.NotEmpty(t, st.Frames) 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) { func (s *SentryTest) TestSentry_CaptureTags() {
err := pkgErrors.New("error") s.gin.GET("/test_taggedError", func(c *gin.Context) {
st := newRavenStackTrace(&raven.Client{}, err, 0) 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) s.sentry.TaggedTypes = SentryTaggedTypes{
assert.NotEmpty(t, st.Frames) 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) { func TestSentry_Suite(t *testing.T) {

View File

@ -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
}

View File

@ -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)
}

View File

@ -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())
}

View File

@ -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)
}

View File

@ -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()
}

View File

@ -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))
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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))
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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,
"<message>: "+err.Error(),
"<wrapped>",
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
}

View File

@ -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)
}

2
go.mod
View File

@ -33,7 +33,7 @@ require (
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 // indirect golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 // indirect
golang.org/x/sys v0.0.0-20200428200454-593003d681fa // indirect golang.org/x/sys v0.0.0-20200428200454-593003d681fa // indirect
golang.org/x/text v0.3.2 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/gormigrate.v1 v1.6.0
gopkg.in/yaml.v2 v2.2.8 gopkg.in/yaml.v2 v2.2.8
) )

29
go.sum
View File

@ -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-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 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.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 h1:88crIK23zO6TqlQBt+f9FrPJNKm9ZEr7qjp9vl/d5TM=
github.com/gin-gonic/gin v1.6.2/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= 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-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-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 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 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 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 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 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 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= 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/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.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.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 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 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= 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.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.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.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-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 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= 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 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= 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.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 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 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= 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/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 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 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 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 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= 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/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/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.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 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= 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-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-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-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 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 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= 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-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191004055002-72853e10c5a3/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 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-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= 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/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 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 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM=
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= 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.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 h1:XpYM6RHQPmzwY7Uyu+t+xxMXc86JYFJn4nEc9HzQjsI=
gopkg.in/gormigrate.v1 v1.6.0/go.mod h1:Lf00lQrHqfSYWiTtPcyQabsDdM6ejZaMgV0OU6JMSlw= 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/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.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 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 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 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 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= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=