mirror of
https://github.com/go-kratos/kratos.git
synced 2025-01-28 03:57:02 +02:00
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
This commit is contained in:
parent
34d9666e0e
commit
9860d59ca7
54
contrib/errortracker/sentry/README.md
Normal file
54
contrib/errortracker/sentry/README.md
Normal file
@ -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/)
|
28
contrib/errortracker/sentry/go.mod
Normal file
28
contrib/errortracker/sentry/go.mod
Normal file
@ -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 => ../../../
|
55
contrib/errortracker/sentry/go.sum
Normal file
55
contrib/errortracker/sentry/go.sum
Normal file
@ -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=
|
145
contrib/errortracker/sentry/sentry.go
Normal file
145
contrib/errortracker/sentry/sentry.go
Normal file
@ -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()
|
||||
}
|
49
contrib/errortracker/sentry/sentry_test.go
Normal file
49
contrib/errortracker/sentry/sentry_test.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user