-
-
Notifications
You must be signed in to change notification settings - Fork 4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(middleware): adding sentry as errortracker (#3122)
* init sentry * rename var * fix ctx key * fix key * update readme * add test * fix ctx args position * update readme * use sprintf * update readme * fix readme * fix fmt * add ut * refactoring get hub * fix docs * fix get hub nil * Update README.md * fix alias
- Loading branch information
Showing
5 changed files
with
331 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
# Sentry middleware for Kratos | ||
This middleware helps you to catch panics and report them to [sentry](https://sentry.io/) | ||
|
||
## Quick Start | ||
You could check the full demo in example folder. | ||
```go | ||
// Step 1: | ||
// init sentry in the entry of your application | ||
import "github.com/getsentry/sentry-go" | ||
|
||
sentry.Init(sentry.ClientOptions{ | ||
Dsn: "<your dsn>", | ||
AttachStacktrace: true, // recommended | ||
}) | ||
|
||
|
||
// Step 2: | ||
// set middleware | ||
import ksentry "github.com/go-kratos/kratos/contrib/errortracker/sentry/v2" | ||
|
||
// for HTTP server, new HTTP server with sentry middleware options | ||
var opts = []http.ServerOption{ | ||
http.Middleware( | ||
recovery.Recovery(), | ||
tracing.Server(), | ||
ksentry.Server(ksentry.WithTags(map[string]interface{}{ | ||
"tag": "some-custom-constant-tag", | ||
"trace_id": tracing.TraceID(), // If you want to use the TraceID valuer, you need to place it after the A middleware. | ||
})), // must after Recovery middleware, because of the exiting order will be reversed | ||
|
||
logging.Server(logger), | ||
), | ||
} | ||
|
||
// for gRPC server, new gRPC server with sentry middleware options | ||
var opts = []grpc.ServerOption{ | ||
grpc.Middleware( | ||
recovery.Recovery(), | ||
tracing.Server(), | ||
ksentry.Server(ksentry.WithTags(map[string]interface{}{ | ||
"tag": "some-custom-constant-tag", | ||
"trace_id": tracing.TraceID(), // If you want to use the TraceID valuer, you need to place it after the A middleware. | ||
})), // must after Recovery middleware, because of the exiting order will be reversed | ||
logging.Server(logger), | ||
), | ||
} | ||
|
||
|
||
// Then, the framework will report events to Sentry when your trigger panics. | ||
// Or your can push events to Sentry manually | ||
``` | ||
|
||
## Reference | ||
* [https://docs.sentry.io/platforms/go/](https://docs.sentry.io/platforms/go/) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
module github.com/go-kratos/kratos/contrib/errortracker/sentry/v2 | ||
|
||
go 1.19 | ||
|
||
require ( | ||
github.com/getsentry/sentry-go v0.25.0 | ||
github.com/go-kratos/kratos/v2 v2.0.0-00010101000000-000000000000 | ||
) | ||
|
||
require ( | ||
github.com/go-kratos/aegis v0.2.0 // indirect | ||
github.com/go-playground/form/v4 v4.2.0 // indirect | ||
github.com/golang/protobuf v1.5.3 // indirect | ||
github.com/google/uuid v1.3.0 // indirect | ||
github.com/gorilla/mux v1.8.1 // indirect | ||
github.com/kr/text v0.2.0 // indirect | ||
golang.org/x/net v0.17.0 // indirect | ||
golang.org/x/sys v0.13.0 // indirect | ||
golang.org/x/text v0.13.0 // indirect | ||
google.golang.org/genproto v0.0.0-20230629202037-9506855d4529 // indirect | ||
google.golang.org/genproto/googleapis/api v0.0.0-20230629202037-9506855d4529 // indirect | ||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230629202037-9506855d4529 // indirect | ||
google.golang.org/grpc v1.56.3 // indirect | ||
google.golang.org/protobuf v1.31.0 // indirect | ||
gopkg.in/yaml.v3 v3.0.1 // indirect | ||
) | ||
|
||
replace github.com/go-kratos/kratos/v2 => ../../../ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g= | ||
github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 h1:/inchEIKaYC1Akx+H+gqO04wryn5h75LSazbRlnya1k= | ||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= | ||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= | ||
github.com/envoyproxy/go-control-plane v0.11.2-0.20230627204322-7d0032219fcb h1:kxNVXsNro/lpR5WD+P1FI/yUHn2G03Glber3k8cQL2Y= | ||
github.com/envoyproxy/protoc-gen-validate v0.10.1 h1:c0g45+xCJhdgFGw7a5QAfdS4byAbud7miNWJ1WwEVf8= | ||
github.com/getsentry/sentry-go v0.25.0 h1:q6Eo+hS+yoJlTO3uu/azhQadsD8V+jQn2D8VvX1eOyI= | ||
github.com/getsentry/sentry-go v0.25.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= | ||
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= | ||
github.com/go-kratos/aegis v0.2.0 h1:dObzCDWn3XVjUkgxyBp6ZeWtx/do0DPZ7LY3yNSJLUQ= | ||
github.com/go-kratos/aegis v0.2.0/go.mod h1:v0R2m73WgEEYB3XYu6aE2WcMwsZkJ/Rzuf5eVccm7bI= | ||
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/form/v4 v4.2.0 h1:N1wh+Goz61e6w66vo8vJkQt+uwZSoLz50kZPJWR8eic= | ||
github.com/go-playground/form/v4 v4.2.0/go.mod h1:q1a2BY+AQUUzhl6xA/6hBetay6dEIhMHjgvJiGo6K7U= | ||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= | ||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= | ||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= | ||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= | ||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= | ||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= | ||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= | ||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= | ||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= | ||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= | ||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= | ||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= | ||
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= | ||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= | ||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | ||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= | ||
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= | ||
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= | ||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= | ||
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= | ||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= | ||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= | ||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||
google.golang.org/genproto v0.0.0-20230629202037-9506855d4529 h1:9JucMWR7sPvCxUFd6UsOUNmA5kCcWOfORaT3tpAsKQs= | ||
google.golang.org/genproto v0.0.0-20230629202037-9506855d4529/go.mod h1:xZnkP7mREFX5MORlOPEzLMr+90PPZQ2QWzrVTWfAq64= | ||
google.golang.org/genproto/googleapis/api v0.0.0-20230629202037-9506855d4529 h1:s5YSX+ZH5b5vS9rnpGymvIyMpLRJizowqDlOuyjXnTk= | ||
google.golang.org/genproto/googleapis/api v0.0.0-20230629202037-9506855d4529/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= | ||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230629202037-9506855d4529 h1:DEH99RbiLZhMxrpEJCZ0A+wdTe0EOgou/poSLx9vWf4= | ||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230629202037-9506855d4529/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= | ||
google.golang.org/grpc v1.56.3 h1:8I4C0Yq1EjstUzUJzpcRVbuYA2mODtEmpWiQoN/b2nc= | ||
google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= | ||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= | ||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= | ||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= | ||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= | ||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= | ||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | ||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
package sentry | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"net" | ||
"os" | ||
"strings" | ||
"time" | ||
|
||
"github.com/getsentry/sentry-go" | ||
|
||
"github.com/go-kratos/kratos/v2/log" | ||
"github.com/go-kratos/kratos/v2/middleware" | ||
"github.com/go-kratos/kratos/v2/transport" | ||
"github.com/go-kratos/kratos/v2/transport/grpc" | ||
"github.com/go-kratos/kratos/v2/transport/http" | ||
) | ||
|
||
type ctxKey struct{} | ||
|
||
type Option func(*options) | ||
|
||
type options struct { | ||
repanic bool | ||
waitForDelivery bool | ||
timeout time.Duration | ||
tags map[string]interface{} | ||
} | ||
|
||
// Repanic configures whether Sentry should repanic after recovery, in most cases it should be set to true. | ||
func WithRepanic(repanic bool) Option { | ||
return func(opts *options) { | ||
opts.repanic = repanic | ||
} | ||
} | ||
|
||
// WaitForDelivery configures whether you want to block the request before moving forward with the response. | ||
func WithWaitForDelivery(waitForDelivery bool) Option { | ||
return func(opts *options) { | ||
opts.waitForDelivery = waitForDelivery | ||
} | ||
} | ||
|
||
// Timeout for the event delivery requests. | ||
func WithTimeout(timeout time.Duration) Option { | ||
return func(opts *options) { | ||
opts.timeout = timeout | ||
} | ||
} | ||
|
||
// Global tags injection, the value type must be string or log.Valuer | ||
func WithTags(kvs map[string]interface{}) Option { | ||
return func(opts *options) { | ||
opts.tags = kvs | ||
} | ||
} | ||
|
||
// Server returns a new server middleware for Sentry. | ||
func Server(opts ...Option) middleware.Middleware { | ||
conf := options{repanic: true} | ||
for _, o := range opts { | ||
o(&conf) | ||
} | ||
if conf.timeout == 0 { | ||
conf.timeout = 2 * time.Second | ||
} | ||
return func(handler middleware.Handler) middleware.Handler { | ||
return func(ctx context.Context, req interface{}) (reply interface{}, err error) { | ||
hub := GetHubFromContext(ctx) | ||
scope := hub.Scope() | ||
|
||
for k, v := range conf.tags { | ||
switch val := v.(type) { | ||
case string: | ||
scope.SetTag(k, val) | ||
case log.Valuer: | ||
scope.SetTag(k, fmt.Sprintf("%v", val(ctx))) | ||
} | ||
} | ||
|
||
if tr, ok := transport.FromServerContext(ctx); ok { | ||
switch tr.Kind() { | ||
case transport.KindGRPC: | ||
gtr := tr.(*grpc.Transport) | ||
scope.SetContext("gRPC", map[string]interface{}{ | ||
"endpoint": gtr.Endpoint(), | ||
"operation": gtr.Operation(), | ||
}) | ||
headers := make(map[string]interface{}) | ||
for _, k := range gtr.RequestHeader().Keys() { | ||
headers[k] = gtr.RequestHeader().Get(k) | ||
} | ||
scope.SetContext("Headers", headers) | ||
case transport.KindHTTP: | ||
htr := tr.(*http.Transport) | ||
r := htr.Request() | ||
scope.SetRequest(r) | ||
} | ||
} | ||
|
||
ctx = context.WithValue(ctx, ctxKey{}, hub) | ||
defer recoverWithSentry(ctx, conf, hub, req) | ||
return handler(ctx, req) | ||
} | ||
} | ||
} | ||
|
||
func recoverWithSentry(ctx context.Context, opts options, hub *sentry.Hub, req interface{}) { | ||
if err := recover(); err != nil { | ||
if !isBrokenPipeError(err) { | ||
eventID := hub.RecoverWithContext( | ||
context.WithValue(ctx, sentry.RequestContextKey, req), | ||
err, | ||
) | ||
if eventID != nil && opts.waitForDelivery { | ||
hub.Flush(opts.timeout) | ||
} | ||
} | ||
if opts.repanic { | ||
panic(err) | ||
} | ||
} | ||
} | ||
|
||
func isBrokenPipeError(err interface{}) bool { | ||
if netErr, ok := err.(*net.OpError); ok { | ||
if sysErr, ok := netErr.Err.(*os.SyscallError); ok { | ||
if strings.Contains(strings.ToLower(sysErr.Error()), "broken pipe") || | ||
strings.Contains(strings.ToLower(sysErr.Error()), "connection reset by peer") { | ||
return true | ||
} | ||
} | ||
} | ||
return false | ||
} | ||
|
||
// GetHubFromContext retrieves attached *sentry.Hub instance from context or sentry. | ||
// You can use this hub for extra information reporting | ||
func GetHubFromContext(ctx context.Context) *sentry.Hub { | ||
if hub, ok := ctx.Value(ctxKey{}).(*sentry.Hub); ok { | ||
return hub | ||
} | ||
return sentry.CurrentHub().Clone() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
package sentry | ||
|
||
import ( | ||
"testing" | ||
"time" | ||
) | ||
|
||
func TestWithTags(t *testing.T) { | ||
opts := new(options) | ||
strval := "bar" | ||
kvs := map[string]interface{}{ | ||
"foo": strval, | ||
} | ||
funcTags := WithTags(kvs) | ||
funcTags(opts) | ||
if opts.tags["foo"].(string) != strval { | ||
t.Errorf("TestWithTags() = %v, want %v", opts.tags["foo"].(string), strval) | ||
} | ||
} | ||
|
||
func TestWithRepanic(t *testing.T) { | ||
opts := new(options) | ||
val := true | ||
f := WithRepanic(val) | ||
f(opts) | ||
if opts.repanic != val { | ||
t.Errorf("TestWithRepanic() = %v, want %v", opts.repanic, val) | ||
} | ||
} | ||
|
||
func TestWithWaitForDelivery(t *testing.T) { | ||
opts := new(options) | ||
val := true | ||
f := WithWaitForDelivery(val) | ||
f(opts) | ||
if opts.waitForDelivery != val { | ||
t.Errorf("TestWithWaitForDelivery() = %v, want %v", opts.waitForDelivery, val) | ||
} | ||
} | ||
|
||
func TestWithTimeout(t *testing.T) { | ||
opts := new(options) | ||
val := time.Second * 10 | ||
f := WithTimeout(val) | ||
f(opts) | ||
if opts.timeout != val { | ||
t.Errorf("TestWithTimeout() = %v, want %v", opts.timeout, val) | ||
} | ||
} |