Skip to content

Commit

Permalink
maintenance/errors: Switch to sentry-go package
Browse files Browse the repository at this point in the history
  • Loading branch information
monstermunchkin committed Jul 18, 2024
1 parent bc8d8f5 commit e93b886
Show file tree
Hide file tree
Showing 2 changed files with 85 additions and 109 deletions.
153 changes: 71 additions & 82 deletions maintenance/errors/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ import (
"os"
"time"

"github.com/getsentry/sentry-go"
"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"
Expand All @@ -30,6 +30,22 @@ func init() {
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{}
Expand Down Expand Up @@ -108,7 +124,7 @@ func HandleError(rp interface{}, handlerName string, w http.ResponseWriter, r *h
}
log.Stack(ctx)

sentryEvent{ctx, r, rp, 1, handlerName}.Send()
handle(ctx, r, rp, 1, handlerName)

runtime.WriteError(w, http.StatusInternalServerError, errors.New("Internal Server Error"))
}
Expand All @@ -124,119 +140,92 @@ 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)
handle(ctx, nil, rp, 1, "")
}

type sentryEvent struct {
ctx context.Context
req *http.Request // optional
r interface{}
level int
handlerName string
}

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 handle(ctx context.Context, r *http.Request, rp any, level int, handlerName string) {
// 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 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)

handle(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
Expand All @@ -248,7 +237,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 {
Expand All @@ -262,7 +251,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")

Expand Down Expand Up @@ -320,11 +309,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
Expand All @@ -333,7 +322,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
Expand Down
41 changes: 14 additions & 27 deletions maintenance/errors/error_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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
}{
{
Expand All @@ -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{}{},
},
},
Expand All @@ -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",
Expand All @@ -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{}{},
},
},
Expand All @@ -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{}{},
Expand Down

0 comments on commit e93b886

Please sign in to comment.