diff --git a/maintenance/errors/error.go b/maintenance/errors/error.go index 77e6265b7..2afb6e819 100644 --- a/maintenance/errors/error.go +++ b/maintenance/errors/error.go @@ -11,12 +11,13 @@ import ( "os" "time" + "github.com/getsentry/sentry-go" + "github.com/prometheus/client_golang/prometheus" + "github.com/rs/zerolog" + "github.com/pace/bricks/http/jsonapi/runtime" "github.com/pace/bricks/http/oauth2" - "github.com/pace/bricks/maintenance/errors/raven" "github.com/pace/bricks/maintenance/log" - "github.com/prometheus/client_golang/prometheus" - "github.com/rs/zerolog" ) var paceHTTPPanicCounter = prometheus.NewGauge(prometheus.GaugeOpts{ @@ -24,10 +25,40 @@ var paceHTTPPanicCounter = prometheus.NewGauge(prometheus.GaugeOpts{ Help: "A counter for panics intercepted while handling a request", }) +var DefaultClient *sentry.Client + func init() { + sentryOptions := sentry.ClientOptions{ + Dsn: os.Getenv("SENTRY_DSN"), + Release: os.Getenv("SENTRY_RELEASE"), + Environment: os.Getenv("ENVIRONMENT"), + SampleRate: 1.0, + } + + err := sentry.Init(sentryOptions) + if err != nil { + panic(fmt.Errorf("failed to set-up sentry client: %w", err)) + } + prometheus.MustRegister(paceHTTPPanicCounter) } +type ErrWithExtra struct { + err error + extra map[string]any +} + +func NewErrWithExtra(err error, extra map[string]any) ErrWithExtra { + return ErrWithExtra{ + err: err, + extra: extra, + } +} + +func (e ErrWithExtra) Error() string { + return e.err.Error() +} + // PanicWrap wraps a panic for HandleRequest type PanicWrap struct { err interface{} @@ -106,7 +137,7 @@ func HandleError(rp interface{}, handlerName string, w http.ResponseWriter, r *h } log.Stack(ctx) - sentryEvent{ctx, r, rp, 1, handlerName}.Send() + sentry.CaptureEvent(getEvent(ctx, r, rp, 1, handlerName)) runtime.WriteError(w, http.StatusInternalServerError, errors.New("Internal Server Error")) } @@ -122,119 +153,94 @@ func Handle(ctx context.Context, rp interface{}) { } log.Stack(ctx) - sentryEvent{ctx, nil, rp, 1, ""}.Send() -} - -// HandleWithCtx should be called with defer to recover panics in goroutines -func HandleWithCtx(ctx context.Context, handlerName string) { - if rp := recover(); rp != nil { - log.Ctx(ctx).Error().Str("handler", handlerName).Msgf("Panic: %v", rp) - log.Stack(ctx) - - sentryEvent{ctx, nil, rp, 2, handlerName}.Send() - } -} - -func HandleErrorNoStack(ctx context.Context, err error) { - log.Ctx(ctx).Info().Msgf("Received error, will not handle further: %v", err) -} - -// New returns an error that formats as the given text. -func New(text string) error { - return errors.New(text) -} - -// WrapWithExtra adds extra data to an error before reporting to Sentry -func WrapWithExtra(err error, extraInfo map[string]interface{}) error { - return raven.WrapWithExtra(err, extraInfo) -} - -type sentryEvent struct { - ctx context.Context - req *http.Request // optional - r interface{} - level int - handlerName string + sentry.CaptureEvent(getEvent(ctx, nil, rp, 1, "")) } -func (e sentryEvent) Send() { - _, errCh := raven.Capture(e.build(), nil) - <-errCh // ensure the message get send even if the main goroutine is about to stop -} - -func (e sentryEvent) build() *raven.Packet { - ctx, r, rp, handlerName := e.ctx, e.req, e.r, e.handlerName - +func getEvent(ctx context.Context, r *http.Request, rp any, level int, handlerName string) *sentry.Event { // get request from context if available if r == nil { r = requestFromContext(ctx) } rvalStr := fmt.Sprint(rp) - var packet *raven.Packet + + event := sentry.NewEvent() if err, ok := rp.(error); ok { - stack := raven.GetOrNewStacktrace(err, 2+e.level, 3, nil) - packet = raven.NewPacket(rvalStr, raven.NewException(err, stack)) + event.SetException(err, level) } else { - stack := raven.NewStacktrace(2+e.level, 3, nil) - packet = raven.NewPacket(rvalStr, raven.NewException(errors.New(rvalStr), stack)) - } - - // extract ErrWithExtra info and append it to the packet - if ee, ok := rp.(raven.ErrWithExtra); ok { - for k, v := range ee.ExtraInfo() { - packet.Extra[k] = v - } + event.SetException(errors.New(rvalStr), level) } // add user userID, ok := oauth2.UserID(ctx) - user := raven.User{ID: userID} - if r != nil { - user.IP = log.ProxyAwareRemote(r) - } - packet.Interfaces = append(packet.Interfaces, &user) if ok { - packet.Tags = append(packet.Tags, raven.Tag{Key: "user_id", Value: userID}) + event.User.ID = userID + } + + if r != nil { + event.User.IPAddress = log.ProxyAwareRemote(r) } // from context if reqID := log.RequestIDFromContext(ctx); reqID != "" { - packet.Extra["req_id"] = reqID - packet.Tags = append(packet.Tags, raven.Tag{Key: "req_id", Value: reqID}) + event.Extra["req_id"] = reqID + event.Tags["req_id"] = reqID } + if traceID := log.TraceIDFromContext(ctx); traceID != "" { - packet.Extra["uber_trace_id"] = traceID - packet.Tags = append(packet.Tags, raven.Tag{Key: "trace_id", Value: traceID}) + event.Extra["uber_trace_id"] = traceID + event.Tags["trace_id"] = traceID } - packet.Extra["handler"] = handlerName + + event.Extra["handler"] = handlerName + if clientID, ok := oauth2.ClientID(ctx); ok { - packet.Extra["oauth2_client_id"] = clientID - } - if scopes := oauth2.Scopes(ctx); len(scopes) > 0 { - packet.Extra["oauth2_scopes"] = scopes + event.Extra["oauth2_client_id"] = clientID } - // from request - if r != nil { - packet.Interfaces = append(packet.Interfaces, raven.NewHttp(r)) + if scopes := oauth2.Scopes(ctx); len(scopes) > 0 { + event.Extra["oauth2_scopes"] = scopes } // from env - packet.Extra["microservice"] = os.Getenv("JAEGER_SERVICE_NAME") + event.Extra["microservice"] = os.Getenv("JAEGER_SERVICE_NAME") // add breadcrumbs - packet.Breadcrumbs = getBreadcrumbs(ctx) + event.Breadcrumbs = getBreadcrumbs(ctx) + + return event +} - return packet +// HandleWithCtx should be called with defer to recover panics in goroutines +func HandleWithCtx(ctx context.Context, handlerName string) { + if rp := recover(); rp != nil { + log.Ctx(ctx).Error().Str("handler", handlerName).Msgf("Panic: %v", rp) + log.Stack(ctx) + + sentry.CaptureEvent(getEvent(ctx, nil, rp, 2, handlerName)) + } +} + +func HandleErrorNoStack(ctx context.Context, err error) { + log.Ctx(ctx).Info().Msgf("Received error, will not handle further: %v", err) +} + +// New returns an error that formats as the given text. +func New(text string) error { + return errors.New(text) +} + +// WrapWithExtra adds extra data to an error before reporting to Sentry +func WrapWithExtra(err error, extraInfo map[string]interface{}) error { + return NewErrWithExtra(err, extraInfo) } // getBreadcrumbs takes a context and tries to extract the logs from it if it // holds a log.Sink. If that's the case, the logs will all be translated // to valid sentry breadcrumbs if possible. In case of a failure, the // breadcrumbs will be dropped and a warning will be logged. -func getBreadcrumbs(ctx context.Context) []*raven.Breadcrumb { +func getBreadcrumbs(ctx context.Context) []*sentry.Breadcrumb { sink, ok := log.SinkFromContext(ctx) if !ok { return nil @@ -246,7 +252,7 @@ func getBreadcrumbs(ctx context.Context) []*raven.Breadcrumb { return nil } - result := make([]*raven.Breadcrumb, len(data)) + result := make([]*sentry.Breadcrumb, len(data)) for i, d := range data { crumb, err := createBreadcrumb(d) if err != nil { @@ -260,7 +266,7 @@ func getBreadcrumbs(ctx context.Context) []*raven.Breadcrumb { return result } -func createBreadcrumb(data map[string]interface{}) (*raven.Breadcrumb, error) { +func createBreadcrumb(data map[string]any) (*sentry.Breadcrumb, error) { // remove the request id if it can still be found in the logs delete(data, "req_id") @@ -318,11 +324,11 @@ func createBreadcrumb(data map[string]interface{}) (*raven.Breadcrumb, error) { typ = "error" } - return &raven.Breadcrumb{ + return &sentry.Breadcrumb{ Category: category, Level: level, Message: message, - Timestamp: time.Unix(), + Timestamp: time, Type: typ, Data: data, }, nil @@ -331,7 +337,7 @@ func createBreadcrumb(data map[string]interface{}) (*raven.Breadcrumb, error) { // translateZerologLevelToSentryLevel takes in a zerolog.Level as string // and returns the equivalent sentry breadcrumb level. If the given level // can't be parsed to a valid zerolog.Level an error is returned. -func translateZerologLevelToSentryLevel(l string) (string, error) { +func translateZerologLevelToSentryLevel(l string) (sentry.Level, error) { level, err := zerolog.ParseLevel(l) if err != nil { return "", err diff --git a/maintenance/errors/error_test.go b/maintenance/errors/error_test.go index dc56c2009..f3f2a57f0 100644 --- a/maintenance/errors/error_test.go +++ b/maintenance/errors/error_test.go @@ -9,13 +9,14 @@ import ( "net/http/httptest" "strings" "testing" + "time" + "github.com/getsentry/sentry-go" "github.com/gorilla/mux" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/pace/bricks/http/transport" - "github.com/pace/bricks/maintenance/errors/raven" "github.com/pace/bricks/maintenance/log" ) @@ -61,28 +62,14 @@ func TestWrapWithExtra(t *testing.T) { } } -func TestStackTrace(t *testing.T) { - e := sentryEvent{ - ctx: context.Background(), - handlerName: "TestStackTrace", - r: nil, - level: 1, - req: nil, - } - pak := e.build() - - d, err := pak.JSON() - assert.NoError(t, err) - - assert.NotContains(t, string(d), `"module":"testing"`) - assert.NotContains(t, string(d), `"filename":"testing/testing.go"`) -} - func Test_createBreadcrumb(t *testing.T) { + tm, err := time.Parse(time.RFC3339, "2020-02-27T10:19:28+01:00") + require.NoError(t, err) + tests := []struct { name string data map[string]interface{} - want *raven.Breadcrumb + want *sentry.Breadcrumb wantErr bool }{ { @@ -93,10 +80,10 @@ func Test_createBreadcrumb(t *testing.T) { "time": "2020-02-27T10:19:28+01:00", "req_id": "bpboj6bipt34r4teo7g0", }, - want: &raven.Breadcrumb{ + want: &sentry.Breadcrumb{ Level: "error", Message: "this is an error message", - Timestamp: 1582795168, + Timestamp: tm, Data: map[string]interface{}{}, }, }, @@ -115,11 +102,11 @@ func Test_createBreadcrumb(t *testing.T) { "url": "https://www.pace.car/", "req_id": "bpboj6bipt34r4teo7g0", }, - want: &raven.Breadcrumb{ + want: &sentry.Breadcrumb{ Category: "http", Level: "debug", Message: "HTTPS GET www.pace.car", - Timestamp: 1582795168, + Timestamp: tm, Type: "http", Data: map[string]interface{}{ "method": "GET", @@ -137,11 +124,11 @@ func Test_createBreadcrumb(t *testing.T) { "message": "this is a panic message", "time": "2020-02-27T10:19:28+01:00", }, - want: &raven.Breadcrumb{ + want: &sentry.Breadcrumb{ Level: "fatal", Type: "error", Message: "this is a panic message", - Timestamp: 1582795168, + Timestamp: tm, Data: map[string]interface{}{}, }, }, @@ -155,10 +142,10 @@ func Test_createBreadcrumb(t *testing.T) { "time": "2020-02-27T10:19:28+01:00", "req_id": "bpboj6bipt34r4teo7g0", }, - want: &raven.Breadcrumb{ + want: &sentry.Breadcrumb{ Category: "redis", Level: "info", - Timestamp: 1582795168, + Timestamp: tm, Message: "this is an error message", Type: "error", Data: map[string]interface{}{},