1
0
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:
Windfarer 2024-01-18 13:54:37 +08:00 committed by GitHub
parent 34d9666e0e
commit 9860d59ca7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 331 additions and 0 deletions

View 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/)

View 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 => ../../../

View 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=

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

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