From 123c12144ed46699dbc9e33a10a17af77207bd1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emir=20Ribi=C4=87?= Date: Wed, 25 Dec 2024 01:33:51 +0100 Subject: [PATCH 1/9] initial grpc work --- logrus/README.md | 1 - sentry.go | 3 +++ tracing.go | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/logrus/README.md b/logrus/README.md index b4444530d..961171a3a 100644 --- a/logrus/README.md +++ b/logrus/README.md @@ -88,4 +88,3 @@ sentryHook.AddTags(map[string]string{ ## Notes - Always call Flush to ensure all events are sent to Sentry before program termination - diff --git a/sentry.go b/sentry.go index 49c172318..afa1a2d8a 100644 --- a/sentry.go +++ b/sentry.go @@ -12,6 +12,9 @@ const SDKVersion = "0.30.0" // sentry-go SDK. const apiVersion = "7" +// DefaultFlushTimeout is the default timeout used for flushing events. +const DefaultFlushTimeout = 2 * time.Second + // Init initializes the SDK with options. The returned error is non-nil if // options is invalid, for instance if a malformed DSN is provided. func Init(options ClientOptions) error { diff --git a/tracing.go b/tracing.go index 0c5877c59..7fe798175 100644 --- a/tracing.go +++ b/tracing.go @@ -30,6 +30,7 @@ const ( SpanOriginStdLib = "auto.http.stdlib" SpanOriginIris = "auto.http.iris" SpanOriginNegroni = "auto.http.negroni" + SpanOriginGrpc = "auto.http.grpc" ) // A Span is the building block of a Sentry transaction. Spans build up a tree From 0712d4cf2f9c70194f1c6d46fb8689051c622ba2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emir=20Ribi=C4=87?= Date: Wed, 25 Dec 2024 01:33:59 +0100 Subject: [PATCH 2/9] initial grpc work --- grpc/README.MD | 164 ++++++++++++++++++++++++++++ grpc/client.go | 120 +++++++++++++++++++++ grpc/go.mod | 18 ++++ grpc/go.sum | 48 +++++++++ grpc/server.go | 286 +++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 636 insertions(+) create mode 100644 grpc/README.MD create mode 100644 grpc/client.go create mode 100644 grpc/go.mod create mode 100644 grpc/go.sum create mode 100644 grpc/server.go diff --git a/grpc/README.MD b/grpc/README.MD new file mode 100644 index 000000000..a4598b1da --- /dev/null +++ b/grpc/README.MD @@ -0,0 +1,164 @@ +

+ + + +
+

+ +# Official Sentry gRPC Interceptor for Sentry-go SDK + +**go.dev:** [https://pkg.go.dev/github.com/getsentry/sentry-go/grpc](https://pkg.go.dev/github.com/getsentry/sentry-go/grpc) + +**Example:** https://github.com/getsentry/sentry-go/tree/master/_examples/grpc + + +## Installation + +```sh +go get github.com/getsentry/sentry-go/grpc +``` + +## Server-Side Usage + +```go +import ( + "fmt" + "net" + + "google.golang.org/grpc" + "google.golang.org/grpc/reflection" + + "github.com/getsentry/sentry-go" + sentrygrpc "github.com/getsentry/sentry-go/grpc" +) + +func main() { + // Initialize Sentry + if err := sentry.Init(sentry.ClientOptions{ + Dsn: "your-public-dsn", + }); err != nil { + fmt.Printf("Sentry initialization failed: %v\n", err) + } + + // Create gRPC server with Sentry interceptors + server := grpc.NewServer( + sentrygrpc.UnaryServerInterceptor(sentrygrpc.ServerOptions{ + Repanic: true, + WaitForDelivery: true, + }), + sentrygrpc.StreamServerInterceptor(sentrygrpc.ServerOptions{ + Repanic: true, + WaitForDelivery: true, + }), + ) + + // Register reflection for debugging + reflection.Register(server) + + // Start the server + listener, err := net.Listen("tcp", ":50051") + if err != nil { + sentry.CaptureException(err) + fmt.Printf("Failed to listen: %v\n", err) + return + } + + fmt.Println("Server running...") + if err := server.Serve(listener); err != nil { + sentry.CaptureException(err) + } +} +``` + + +## Client-Side Usage + +```go +import ( + "context" + "fmt" + + "google.golang.org/grpc" + + "github.com/getsentry/sentry-go" + sentrygrpc "github.com/getsentry/sentry-go/grpc" +) + +func main() { + // Initialize Sentry + if err := sentry.Init(sentry.ClientOptions{ + Dsn: "your-public-dsn", + }); err != nil { + fmt.Printf("Sentry initialization failed: %v\n", err) + } + + // Create gRPC client with Sentry interceptors + conn, err := grpc.Dial( + "localhost:50051", + grpc.WithInsecure(), + grpc.WithUnaryInterceptor(sentrygrpc.UnaryClientInterceptor(sentrygrpc.ClientOptions{})), + grpc.WithStreamInterceptor(sentrygrpc.StreamClientInterceptor(sentrygrpc.ClientOptions{})), + ) + if err != nil { + sentry.CaptureException(err) + fmt.Printf("Failed to connect: %v\n", err) + return + } + defer conn.Close() + + client := NewYourServiceClient(conn) + + // Make a request + _, err = client.YourMethod(context.Background(), &YourRequest{}) + if err != nil { + sentry.CaptureException(err) + fmt.Printf("Error calling method: %v\n", err) + } +} +``` + +## Configuration + +Both the server and client interceptors accept options for customization: + +### Server Options + +```go +type ServerOptions struct { + // Repanic determines whether the application should re-panic after recovery. + Repanic bool + + // WaitForDelivery determines if the interceptor should block until events are sent to Sentry. + WaitForDelivery bool + + // Timeout sets the maximum duration for Sentry event delivery. + Timeout time.Duration + + // ReportOn defines the conditions under which errors are reported to Sentry. + ReportOn func(error) bool + + // CaptureRequestBody determines whether to capture and send request bodies to Sentry. + CaptureRequestBody bool + + // OperationName overrides the default operation name (grpc.server). + OperationName string +} +``` + +### Client Options + +```go +type ClientOptions struct { + // ReportOn defines the conditions under which errors are reported to Sentry. + ReportOn func(error) bool + + // OperationName overrides the default operation name (grpc.client). + OperationName string +} +``` + +## Notes + +- The interceptors automatically create and manage a Sentry *Hub for each gRPC request or stream. +- Use the Sentry SDK’s context-based APIs to capture exceptions and add additional context. +- Ensure you handle the context correctly to propagate tracing information across requests. diff --git a/grpc/client.go b/grpc/client.go new file mode 100644 index 000000000..e16e4ee63 --- /dev/null +++ b/grpc/client.go @@ -0,0 +1,120 @@ +package sentrygrpc + +import ( + "context" + + "github.com/getsentry/sentry-go" + "google.golang.org/grpc" + "google.golang.org/grpc/metadata" +) + +const defaultClientOperationName = "grpc.client" + +type ClientOptions struct { + // ReportOn defines the conditions under which errors are reported to Sentry. + ReportOn func(error) bool + + // OperationName overrides the default operation name (grpc.client). + OperationName string +} + +func (o *ClientOptions) SetDefaults() { + if o.ReportOn == nil { + o.ReportOn = func(err error) bool { + return true + } + } + if o.OperationName == "" { + o.OperationName = defaultClientOperationName + } +} + +func createOrUpdateMetadata(ctx context.Context, span *sentry.Span) context.Context { + md, ok := metadata.FromOutgoingContext(ctx) + if ok { + md = md.Copy() + md.Append(sentry.SentryTraceHeader, span.ToSentryTrace()) + md.Append(sentry.SentryBaggageHeader, span.ToBaggage()) + } else { + md = metadata.Pairs( + sentry.SentryTraceHeader, span.ToSentryTrace(), + sentry.SentryBaggageHeader, span.ToBaggage(), + ) + } + return metadata.NewOutgoingContext(ctx, md) +} + +func getOrCreateHub(ctx context.Context) (*sentry.Hub, context.Context) { + hub := sentry.GetHubFromContext(ctx) + if hub == nil { + hub = sentry.CurrentHub().Clone() + ctx = sentry.SetHubOnContext(ctx, hub) + } + return hub, ctx +} + +func UnaryClientInterceptor(o ClientOptions) grpc.UnaryClientInterceptor { + o.SetDefaults() + return func(ctx context.Context, + method string, + req, reply interface{}, + cc *grpc.ClientConn, + invoker grpc.UnaryInvoker, + callOpts ...grpc.CallOption) error { + + hub, ctx := getOrCreateHub(ctx) + + span := sentry.StartSpan(ctx, o.OperationName, sentry.WithDescription(method)) + span.SetData("grpc.request.method", method) + ctx = span.Context() + + ctx = createOrUpdateMetadata(ctx, span) + defer span.Finish() + + err := invoker(ctx, method, req, reply, cc, callOpts...) + + if err != nil && o.ReportOn(err) { + hub.WithScope(func(scope *sentry.Scope) { + scope.SetTag("grpc.method", method) + scope.SetContext("request", map[string]any{ + "method": method, + "request": req, + }) + hub.CaptureException(err) + }) + } + + return err + } +} + +func StreamClientInterceptor(o ClientOptions) grpc.StreamClientInterceptor { + o.SetDefaults() + return func(ctx context.Context, + desc *grpc.StreamDesc, + cc *grpc.ClientConn, + method string, + streamer grpc.Streamer, + callOpts ...grpc.CallOption) (grpc.ClientStream, error) { + + hub, ctx := getOrCreateHub(ctx) + + span := sentry.StartSpan(ctx, o.OperationName, sentry.WithDescription(method)) + span.SetData("grpc.request.method", method) + ctx = span.Context() + + ctx = createOrUpdateMetadata(ctx, span) + defer span.Finish() + + clientStream, err := streamer(ctx, desc, cc, method, callOpts...) + + if err != nil && o.ReportOn(err) { + hub.WithScope(func(scope *sentry.Scope) { + scope.SetTag("grpc.method", method) + hub.CaptureException(err) + }) + } + + return clientStream, err + } +} diff --git a/grpc/go.mod b/grpc/go.mod new file mode 100644 index 000000000..72975cf25 --- /dev/null +++ b/grpc/go.mod @@ -0,0 +1,18 @@ +module github.com/getsentry/sentry-go/grpc + +go 1.22 + +replace github.com/getsentry/sentry-go => ../ + +require ( + github.com/getsentry/sentry-go v0.30.0 + google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 + google.golang.org/grpc v1.69.2 +) + +require ( + golang.org/x/net v0.30.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/text v0.19.0 // indirect + google.golang.org/protobuf v1.35.1 // indirect +) diff --git a/grpc/go.sum b/grpc/go.sum new file mode 100644 index 000000000..162aadade --- /dev/null +++ b/grpc/go.sum @@ -0,0 +1,48 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= +github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= +github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY= +go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE= +go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE= +go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY= +go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk= +go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0= +go.opentelemetry.io/otel/sdk/metric v1.31.0 h1:i9hxxLJF/9kkvfHppyLL55aW7iIJz4JjxTeYusH7zMc= +go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8= +go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys= +go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 h1:X58yt85/IXCx0Y3ZwN6sEIKZzQtDEYaBWrDvErdXrRE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= +google.golang.org/grpc v1.69.2 h1:U3S9QEtbXC0bYNvRtcoklF3xGtLViumSYxWykJS+7AU= +google.golang.org/grpc v1.69.2/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/grpc/server.go b/grpc/server.go new file mode 100644 index 000000000..343ce7058 --- /dev/null +++ b/grpc/server.go @@ -0,0 +1,286 @@ +package sentrygrpc + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/getsentry/sentry-go" + "google.golang.org/genproto/googleapis/rpc/errdetails" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" +) + +const ( + sdkIdentifier = "sentry.go.grpc" + defaultServerOperationName = "grpc.server" +) + +type ServerOptions struct { + // Repanic determines whether the application should re-panic after recovery. + Repanic bool + + // WaitForDelivery determines if the interceptor should block until events are sent to Sentry. + WaitForDelivery bool + + // Timeout sets the maximum duration for Sentry event delivery. + Timeout time.Duration + + // ReportOn defines the conditions under which errors are reported to Sentry. + ReportOn func(error) bool + + // CaptureRequestBody determines whether to capture and send request bodies to Sentry. + CaptureRequestBody bool + + // OperationName overrides the default operation name (grpc.server). + OperationName string +} + +func (o *ServerOptions) SetDefaults() { + if o.ReportOn == nil { + o.ReportOn = func(err error) bool { + return true + } + } + + if o.Timeout == 0 { + o.Timeout = sentry.DefaultFlushTimeout + } + + if o.OperationName == "" { + o.OperationName = defaultServerOperationName + } +} + +func recoverWithSentry(ctx context.Context, hub *sentry.Hub, o ServerOptions) { + if r := recover(); r != nil { + eventID := hub.RecoverWithContext(ctx, r) + + if eventID != nil && o.WaitForDelivery { + hub.Flush(o.Timeout) + } + + if o.Repanic { + panic(r) + } + } +} + +func reportErrorToSentry(hub *sentry.Hub, err error, methodName string, req interface{}, md map[string]string) { + hub.WithScope(func(scope *sentry.Scope) { + scope.SetExtras(map[string]any{ + "grpc.method": methodName, + "grpc.error": err.Error(), + }) + + if req != nil { + scope.SetExtra("request", req) + } + + if len(md) > 0 { + scope.SetExtra("metadata", md) + } + + defer hub.CaptureException(err) + + statusErr, ok := status.FromError(err) + if !ok { + return + } + + for _, detail := range statusErr.Details() { + debugInfo, ok := detail.(*errdetails.DebugInfo) + if !ok { + continue + } + hub.AddBreadcrumb(&sentry.Breadcrumb{ + Type: "debug", + Category: "grpc.server", + Message: debugInfo.Detail, + Data: map[string]any{"stackTrace": strings.Join(debugInfo.StackEntries, "\n")}, + Level: sentry.LevelError, + Timestamp: time.Now(), + }, nil) + } + }) +} + +func UnaryServerInterceptor(opts ServerOptions) grpc.UnaryServerInterceptor { + opts.SetDefaults() + + return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { + hub := sentry.GetHubFromContext(ctx) + if hub == nil { + hub = sentry.CurrentHub().Clone() + } + + if client := hub.Client(); client != nil { + client.SetSDKIdentifier(sdkIdentifier) + } + + md, ok := metadata.FromIncomingContext(ctx) + var sentryTraceHeader, sentryBaggageHeader string + data := make(map[string]string) + if ok { + sentryTraceHeader = getFirstHeader(md, sentry.SentryTraceHeader) + sentryBaggageHeader = getFirstHeader(md, sentry.SentryBaggageHeader) + + for k, v := range md { + data[k] = strings.Join(v, ",") + } + } + + options := []sentry.SpanOption{ + sentry.ContinueTrace(hub, sentryTraceHeader, sentryBaggageHeader), + sentry.WithOpName("http.server"), + sentry.WithDescription(info.FullMethod), + sentry.WithTransactionSource(sentry.SourceURL), + } + + transaction := sentry.StartTransaction( + sentry.SetHubOnContext(ctx, hub), + fmt.Sprintf("%s %s", "UnaryServerInterceptor", info.FullMethod), + options..., + ) + + transaction.SetData("http.request.method", info.FullMethod) + + ctx = transaction.Context() + defer transaction.Finish() + + if opts.CaptureRequestBody { + // Marshal from proto.Message to bytes? Slow? + // hub.Scope().SetRequestBody(req) + } + + defer recoverWithSentry(ctx, hub, opts) + + resp, err := handler(ctx, req) + if err != nil && opts.ReportOn(err) { + reportErrorToSentry(hub, err, info.FullMethod, req, data) + + transaction.Sampled = sentry.SampledTrue + } + + statusCode := status.Code(err) + transaction.Status = toSpanStatus(statusCode) + transaction.SetData("http.response.status_code", statusCode.String()) + + return resp, err + } +} + +// StreamServerInterceptor provides Sentry integration for streaming gRPC calls. +func StreamServerInterceptor(opts ServerOptions) grpc.StreamServerInterceptor { + opts.SetDefaults() + return func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { + ctx := ss.Context() + hub := sentry.GetHubFromContext(ctx) + if hub == nil { + hub = sentry.CurrentHub().Clone() + } + + if client := hub.Client(); client != nil { + client.SetSDKIdentifier(sdkIdentifier) + } + + md, ok := metadata.FromIncomingContext(ctx) + var sentryTraceHeader, sentryBaggageHeader string + data := make(map[string]string) + if ok { + sentryTraceHeader = getFirstHeader(md, sentry.SentryTraceHeader) + sentryBaggageHeader = getFirstHeader(md, sentry.SentryBaggageHeader) + + for k, v := range md { + data[k] = strings.Join(v, ",") + } + } + + options := []sentry.SpanOption{ + sentry.ContinueTrace(hub, sentryTraceHeader, sentryBaggageHeader), + sentry.WithOpName("http.server"), + sentry.WithDescription(info.FullMethod), + sentry.WithTransactionSource(sentry.SourceURL), + } + + transaction := sentry.StartTransaction( + sentry.SetHubOnContext(ctx, hub), + fmt.Sprintf("%s %s", "StreamServerInterceptor", info.FullMethod), + options..., + ) + + transaction.SetData("grpc.method", info.FullMethod) + ctx = transaction.Context() + defer transaction.Finish() + + stream := wrapServerStream(ss, ctx) + + defer recoverWithSentry(ctx, hub, opts) + + err := handler(srv, stream) + if err != nil && opts.ReportOn(err) { + reportErrorToSentry(hub, err, info.FullMethod, nil, data) + + transaction.Sampled = sentry.SampledTrue + } + + statusCode := status.Code(err) + transaction.Status = toSpanStatus(statusCode) + transaction.SetData("grpc.status", statusCode.String()) + + return err + } +} + +func getFirstHeader(md metadata.MD, key string) string { + if values := md.Get(key); len(values) > 0 { + return values[0] + } + return "" +} + +// wrapServerStream wraps a grpc.ServerStream, allowing you to inject a custom context. +func wrapServerStream(ss grpc.ServerStream, ctx context.Context) grpc.ServerStream { + return &wrappedServerStream{ServerStream: ss, ctx: ctx} +} + +// wrappedServerStream is a wrapper around grpc.ServerStream that overrides the Context method. +type wrappedServerStream struct { + grpc.ServerStream + ctx context.Context +} + +// Context returns the custom context for the stream. +func (w *wrappedServerStream) Context() context.Context { + return w.ctx +} + +var codeToSpanStatus = map[codes.Code]sentry.SpanStatus{ + codes.OK: sentry.SpanStatusOK, + codes.Canceled: sentry.SpanStatusCanceled, + codes.Unknown: sentry.SpanStatusUnknown, + codes.InvalidArgument: sentry.SpanStatusInvalidArgument, + codes.DeadlineExceeded: sentry.SpanStatusDeadlineExceeded, + codes.NotFound: sentry.SpanStatusNotFound, + codes.AlreadyExists: sentry.SpanStatusAlreadyExists, + codes.PermissionDenied: sentry.SpanStatusPermissionDenied, + codes.ResourceExhausted: sentry.SpanStatusResourceExhausted, + codes.FailedPrecondition: sentry.SpanStatusFailedPrecondition, + codes.Aborted: sentry.SpanStatusAborted, + codes.OutOfRange: sentry.SpanStatusOutOfRange, + codes.Unimplemented: sentry.SpanStatusUnimplemented, + codes.Internal: sentry.SpanStatusInternalError, + codes.Unavailable: sentry.SpanStatusUnavailable, + codes.DataLoss: sentry.SpanStatusDataLoss, + codes.Unauthenticated: sentry.SpanStatusUnauthenticated, +} + +func toSpanStatus(code codes.Code) sentry.SpanStatus { + if status, ok := codeToSpanStatus[code]; ok { + return status + } + return sentry.SpanStatusUndefined +} From cc9547cccbec9bd3a7b79d28fa9b5fb10ce8cf49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emir=20Ribi=C4=87?= Date: Fri, 27 Dec 2024 09:51:45 +0100 Subject: [PATCH 3/9] add grpc integration --- .craft.yml | 3 + _examples/grpc_client/main.go | 116 ++++++ _examples/grpc_server/example.proto | 21 + _examples/grpc_server/examplepb/example.pb.go | 191 +++++++++ .../grpc_server/examplepb/example_grpc.pb.go | 158 ++++++++ _examples/grpc_server/main.go | 95 +++++ grpc/client.go | 15 +- grpc/client_test.go | 235 ++++++++++++ grpc/go.mod | 5 + grpc/go.sum | 17 + grpc/server.go | 10 +- grpc/server_test.go | 362 ++++++++++++++++++ mocks_test.go => mocks.go | 1 - otel/event_processor_test.go | 2 +- otel/helpers_test.go | 36 -- otel/span_processor_test.go | 6 +- 16 files changed, 1222 insertions(+), 51 deletions(-) create mode 100644 _examples/grpc_client/main.go create mode 100644 _examples/grpc_server/example.proto create mode 100644 _examples/grpc_server/examplepb/example.pb.go create mode 100644 _examples/grpc_server/examplepb/example_grpc.pb.go create mode 100644 _examples/grpc_server/main.go create mode 100644 grpc/client_test.go create mode 100644 grpc/server_test.go rename mocks_test.go => mocks.go (99%) diff --git a/.craft.yml b/.craft.yml index 5786bba22..803938afa 100644 --- a/.craft.yml +++ b/.craft.yml @@ -35,6 +35,9 @@ targets: - name: github tagPrefix: zerolog/v tagOnly: true + - name: github + tagPrefix: grpc/v + tagOnly: true - name: registry sdks: github:getsentry/sentry-go: diff --git a/_examples/grpc_client/main.go b/_examples/grpc_client/main.go new file mode 100644 index 000000000..144ad59d2 --- /dev/null +++ b/_examples/grpc_client/main.go @@ -0,0 +1,116 @@ +package main + +import ( + "context" + "fmt" + "log" + "time" + + "grpcdemo/cmd/server/examplepb" + + "github.com/getsentry/sentry-go" + sentrygrpc "github.com/getsentry/sentry-go/grpc" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/metadata" +) + +const grpcServerAddress = "localhost:50051" + +func main() { + // Initialize Sentry + err := sentry.Init(sentry.ClientOptions{ + Dsn: "", + TracesSampleRate: 1.0, + }) + if err != nil { + log.Fatalf("sentry.Init: %s", err) + } + defer sentry.Flush(2 * time.Second) + + // Create a connection to the gRPC server with Sentry interceptors + conn, err := grpc.NewClient( + grpcServerAddress, + grpc.WithTransportCredentials(insecure.NewCredentials()), // Use TLS in production + grpc.WithUnaryInterceptor(sentrygrpc.UnaryClientInterceptor(sentrygrpc.ClientOptions{})), + grpc.WithStreamInterceptor(sentrygrpc.StreamClientInterceptor(sentrygrpc.ClientOptions{})), + ) + if err != nil { + log.Fatalf("Failed to connect to gRPC server: %s", err) + } + defer conn.Close() + + // Create a client for the ExampleService + client := examplepb.NewExampleServiceClient(conn) + + // Perform Unary call + fmt.Println("Performing Unary Call:") + unaryExample(client) + + // Perform Streaming call + fmt.Println("\nPerforming Streaming Call:") + streamExample(client) +} + +func unaryExample(client examplepb.ExampleServiceClient) { + ctx := context.Background() + + // Add metadata to the context + ctx = metadata.NewOutgoingContext(ctx, metadata.Pairs( + "custom-header", "value", + )) + + req := &examplepb.ExampleRequest{ + Message: "Hello, server!", // Change to "error" to simulate an error + } + + res, err := client.UnaryExample(ctx, req) + if err != nil { + fmt.Printf("Unary Call Error: %v\n", err) + sentry.CaptureException(err) + return + } + + fmt.Printf("Unary Response: %s\n", res.Message) +} + +func streamExample(client examplepb.ExampleServiceClient) { + ctx := context.Background() + + // Add metadata to the context + ctx = metadata.NewOutgoingContext(ctx, metadata.Pairs( + "streaming-header", "stream-value", + )) + + stream, err := client.StreamExample(ctx) + if err != nil { + fmt.Printf("Failed to establish stream: %v\n", err) + sentry.CaptureException(err) + return + } + + // Send multiple messages in the stream + messages := []string{"Message 1", "Message 2", "error", "Message 4"} + for _, msg := range messages { + err := stream.Send(&examplepb.ExampleRequest{Message: msg}) + if err != nil { + fmt.Printf("Stream Send Error: %v\n", err) + sentry.CaptureException(err) + return + } + } + + // Close the stream for sending + stream.CloseSend() + + // Receive responses from the server + for { + res, err := stream.Recv() + if err != nil { + fmt.Printf("Stream Recv Error: %v\n", err) + sentry.CaptureException(err) + break + } + fmt.Printf("Stream Response: %s\n", res.Message) + } +} diff --git a/_examples/grpc_server/example.proto b/_examples/grpc_server/example.proto new file mode 100644 index 000000000..356d58f11 --- /dev/null +++ b/_examples/grpc_server/example.proto @@ -0,0 +1,21 @@ +syntax = "proto3"; + +package main; + +option go_package = "github.com/your-username/your-repo/examplepb;examplepb"; + +// ExampleService defines the gRPC service. +service ExampleService { + rpc UnaryExample(ExampleRequest) returns (ExampleResponse); + rpc StreamExample(stream ExampleRequest) returns (stream ExampleResponse); +} + +// ExampleRequest is the request message. +message ExampleRequest { + string message = 1; +} + +// ExampleResponse is the response message. +message ExampleResponse { + string message = 1; +} diff --git a/_examples/grpc_server/examplepb/example.pb.go b/_examples/grpc_server/examplepb/example.pb.go new file mode 100644 index 000000000..84d8b8fbb --- /dev/null +++ b/_examples/grpc_server/examplepb/example.pb.go @@ -0,0 +1,191 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.1 +// protoc v5.29.2 +// source: example.proto + +package examplepb + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// ExampleRequest is the request message. +type ExampleRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExampleRequest) Reset() { + *x = ExampleRequest{} + mi := &file_example_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExampleRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExampleRequest) ProtoMessage() {} + +func (x *ExampleRequest) ProtoReflect() protoreflect.Message { + mi := &file_example_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExampleRequest.ProtoReflect.Descriptor instead. +func (*ExampleRequest) Descriptor() ([]byte, []int) { + return file_example_proto_rawDescGZIP(), []int{0} +} + +func (x *ExampleRequest) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +// ExampleResponse is the response message. +type ExampleResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExampleResponse) Reset() { + *x = ExampleResponse{} + mi := &file_example_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExampleResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExampleResponse) ProtoMessage() {} + +func (x *ExampleResponse) ProtoReflect() protoreflect.Message { + mi := &file_example_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExampleResponse.ProtoReflect.Descriptor instead. +func (*ExampleResponse) Descriptor() ([]byte, []int) { + return file_example_proto_rawDescGZIP(), []int{1} +} + +func (x *ExampleResponse) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +var File_example_proto protoreflect.FileDescriptor + +var file_example_proto_rawDesc = []byte{ + 0x0a, 0x0d, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, + 0x04, 0x6d, 0x61, 0x69, 0x6e, 0x22, 0x2a, 0x0a, 0x0e, 0x45, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, + 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, + 0x65, 0x22, 0x2b, 0x0a, 0x0f, 0x45, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x32, 0x8f, + 0x01, 0x0a, 0x0e, 0x45, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x12, 0x3b, 0x0a, 0x0c, 0x55, 0x6e, 0x61, 0x72, 0x79, 0x45, 0x78, 0x61, 0x6d, 0x70, 0x6c, + 0x65, 0x12, 0x14, 0x2e, 0x6d, 0x61, 0x69, 0x6e, 0x2e, 0x45, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x6d, 0x61, 0x69, 0x6e, 0x2e, 0x45, + 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x40, + 0x0a, 0x0d, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x45, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x12, + 0x14, 0x2e, 0x6d, 0x61, 0x69, 0x6e, 0x2e, 0x45, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x6d, 0x61, 0x69, 0x6e, 0x2e, 0x45, 0x78, 0x61, + 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x28, 0x01, 0x30, 0x01, + 0x42, 0x38, 0x5a, 0x36, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x79, + 0x6f, 0x75, 0x72, 0x2d, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x2f, 0x79, 0x6f, 0x75, + 0x72, 0x2d, 0x72, 0x65, 0x70, 0x6f, 0x2f, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x70, 0x62, + 0x3b, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, +} + +var ( + file_example_proto_rawDescOnce sync.Once + file_example_proto_rawDescData = file_example_proto_rawDesc +) + +func file_example_proto_rawDescGZIP() []byte { + file_example_proto_rawDescOnce.Do(func() { + file_example_proto_rawDescData = protoimpl.X.CompressGZIP(file_example_proto_rawDescData) + }) + return file_example_proto_rawDescData +} + +var file_example_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_example_proto_goTypes = []any{ + (*ExampleRequest)(nil), // 0: main.ExampleRequest + (*ExampleResponse)(nil), // 1: main.ExampleResponse +} +var file_example_proto_depIdxs = []int32{ + 0, // 0: main.ExampleService.UnaryExample:input_type -> main.ExampleRequest + 0, // 1: main.ExampleService.StreamExample:input_type -> main.ExampleRequest + 1, // 2: main.ExampleService.UnaryExample:output_type -> main.ExampleResponse + 1, // 3: main.ExampleService.StreamExample:output_type -> main.ExampleResponse + 2, // [2:4] is the sub-list for method output_type + 0, // [0:2] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_example_proto_init() } +func file_example_proto_init() { + if File_example_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_example_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_example_proto_goTypes, + DependencyIndexes: file_example_proto_depIdxs, + MessageInfos: file_example_proto_msgTypes, + }.Build() + File_example_proto = out.File + file_example_proto_rawDesc = nil + file_example_proto_goTypes = nil + file_example_proto_depIdxs = nil +} diff --git a/_examples/grpc_server/examplepb/example_grpc.pb.go b/_examples/grpc_server/examplepb/example_grpc.pb.go new file mode 100644 index 000000000..56f4b3504 --- /dev/null +++ b/_examples/grpc_server/examplepb/example_grpc.pb.go @@ -0,0 +1,158 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc v5.29.2 +// source: example.proto + +package examplepb + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + ExampleService_UnaryExample_FullMethodName = "/main.ExampleService/UnaryExample" + ExampleService_StreamExample_FullMethodName = "/main.ExampleService/StreamExample" +) + +// ExampleServiceClient is the client API for ExampleService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// ExampleService defines the gRPC service. +type ExampleServiceClient interface { + UnaryExample(ctx context.Context, in *ExampleRequest, opts ...grpc.CallOption) (*ExampleResponse, error) + StreamExample(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[ExampleRequest, ExampleResponse], error) +} + +type exampleServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewExampleServiceClient(cc grpc.ClientConnInterface) ExampleServiceClient { + return &exampleServiceClient{cc} +} + +func (c *exampleServiceClient) UnaryExample(ctx context.Context, in *ExampleRequest, opts ...grpc.CallOption) (*ExampleResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ExampleResponse) + err := c.cc.Invoke(ctx, ExampleService_UnaryExample_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *exampleServiceClient) StreamExample(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[ExampleRequest, ExampleResponse], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &ExampleService_ServiceDesc.Streams[0], ExampleService_StreamExample_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[ExampleRequest, ExampleResponse]{ClientStream: stream} + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type ExampleService_StreamExampleClient = grpc.BidiStreamingClient[ExampleRequest, ExampleResponse] + +// ExampleServiceServer is the server API for ExampleService service. +// All implementations must embed UnimplementedExampleServiceServer +// for forward compatibility. +// +// ExampleService defines the gRPC service. +type ExampleServiceServer interface { + UnaryExample(context.Context, *ExampleRequest) (*ExampleResponse, error) + StreamExample(grpc.BidiStreamingServer[ExampleRequest, ExampleResponse]) error + mustEmbedUnimplementedExampleServiceServer() +} + +// UnimplementedExampleServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedExampleServiceServer struct{} + +func (UnimplementedExampleServiceServer) UnaryExample(context.Context, *ExampleRequest) (*ExampleResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method UnaryExample not implemented") +} +func (UnimplementedExampleServiceServer) StreamExample(grpc.BidiStreamingServer[ExampleRequest, ExampleResponse]) error { + return status.Errorf(codes.Unimplemented, "method StreamExample not implemented") +} +func (UnimplementedExampleServiceServer) mustEmbedUnimplementedExampleServiceServer() {} +func (UnimplementedExampleServiceServer) testEmbeddedByValue() {} + +// UnsafeExampleServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to ExampleServiceServer will +// result in compilation errors. +type UnsafeExampleServiceServer interface { + mustEmbedUnimplementedExampleServiceServer() +} + +func RegisterExampleServiceServer(s grpc.ServiceRegistrar, srv ExampleServiceServer) { + // If the following call pancis, it indicates UnimplementedExampleServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&ExampleService_ServiceDesc, srv) +} + +func _ExampleService_UnaryExample_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ExampleRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ExampleServiceServer).UnaryExample(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ExampleService_UnaryExample_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ExampleServiceServer).UnaryExample(ctx, req.(*ExampleRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ExampleService_StreamExample_Handler(srv interface{}, stream grpc.ServerStream) error { + return srv.(ExampleServiceServer).StreamExample(&grpc.GenericServerStream[ExampleRequest, ExampleResponse]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type ExampleService_StreamExampleServer = grpc.BidiStreamingServer[ExampleRequest, ExampleResponse] + +// ExampleService_ServiceDesc is the grpc.ServiceDesc for ExampleService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var ExampleService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "main.ExampleService", + HandlerType: (*ExampleServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "UnaryExample", + Handler: _ExampleService_UnaryExample_Handler, + }, + }, + Streams: []grpc.StreamDesc{ + { + StreamName: "StreamExample", + Handler: _ExampleService_StreamExample_Handler, + ServerStreams: true, + ClientStreams: true, + }, + }, + Metadata: "example.proto", +} diff --git a/_examples/grpc_server/main.go b/_examples/grpc_server/main.go new file mode 100644 index 000000000..defe1b09a --- /dev/null +++ b/_examples/grpc_server/main.go @@ -0,0 +1,95 @@ +package main + +import ( + "context" + "fmt" + "grpcdemo/cmd/server/examplepb" + "log" + "net" + "time" + + "github.com/getsentry/sentry-go" + sentrygrpc "github.com/getsentry/sentry-go/grpc" + "google.golang.org/grpc" + "google.golang.org/grpc/metadata" +) + +const grpcPort = ":50051" + +// ExampleServiceServer is the server implementation for the ExampleService. +type ExampleServiceServer struct { + examplepb.UnimplementedExampleServiceServer +} + +// UnaryExample handles unary gRPC requests. +func (s *ExampleServiceServer) UnaryExample(ctx context.Context, req *examplepb.ExampleRequest) (*examplepb.ExampleResponse, error) { + md, _ := metadata.FromIncomingContext(ctx) + fmt.Printf("Received Unary Request: %v\nMetadata: %v\n", req.Message, md) + + // Simulate an error for demonstration + if req.Message == "error" { + return nil, fmt.Errorf("simulated unary error") + } + + return &examplepb.ExampleResponse{Message: fmt.Sprintf("Hello, %s!", req.Message)}, nil +} + +// StreamExample handles bidirectional streaming gRPC requests. +func (s *ExampleServiceServer) StreamExample(stream examplepb.ExampleService_StreamExampleServer) error { + for { + req, err := stream.Recv() + if err != nil { + fmt.Printf("Stream Recv Error: %v\n", err) + return err + } + + fmt.Printf("Received Stream Message: %v\n", req.Message) + + if req.Message == "error" { + return fmt.Errorf("simulated stream error") + } + + err = stream.Send(&examplepb.ExampleResponse{Message: fmt.Sprintf("Echo: %s", req.Message)}) + if err != nil { + fmt.Printf("Stream Send Error: %v\n", err) + return err + } + } +} + +func main() { + // Initialize Sentry + err := sentry.Init(sentry.ClientOptions{ + Dsn: "", + TracesSampleRate: 1.0, + }) + if err != nil { + log.Fatalf("sentry.Init: %s", err) + } + defer sentry.Flush(2 * time.Second) + + // Create a new gRPC server with Sentry interceptors + server := grpc.NewServer( + grpc.UnaryInterceptor(sentrygrpc.UnaryServerInterceptor(sentrygrpc.ServerOptions{ + Repanic: true, + CaptureRequestBody: true, + })), + grpc.StreamInterceptor(sentrygrpc.StreamServerInterceptor(sentrygrpc.ServerOptions{ + Repanic: true, + })), + ) + + // Register the ExampleService + examplepb.RegisterExampleServiceServer(server, &ExampleServiceServer{}) + + // Start the server + listener, err := net.Listen("tcp", grpcPort) + if err != nil { + log.Fatalf("Failed to listen on port %s: %v", grpcPort, err) + } + + fmt.Printf("gRPC server is running on %s\n", grpcPort) + if err := server.Serve(listener); err != nil { + log.Fatalf("Failed to serve: %v", err) + } +} diff --git a/grpc/client.go b/grpc/client.go index e16e4ee63..1601ffa19 100644 --- a/grpc/client.go +++ b/grpc/client.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Part of this code is derived from [github.com/johnbellone/grpc-middleware-sentry], licensed under the Apache 2.0 License. + package sentrygrpc import ( @@ -35,12 +38,14 @@ func createOrUpdateMetadata(ctx context.Context, span *sentry.Span) context.Cont md = md.Copy() md.Append(sentry.SentryTraceHeader, span.ToSentryTrace()) md.Append(sentry.SentryBaggageHeader, span.ToBaggage()) - } else { - md = metadata.Pairs( - sentry.SentryTraceHeader, span.ToSentryTrace(), - sentry.SentryBaggageHeader, span.ToBaggage(), - ) + return metadata.NewOutgoingContext(ctx, md) } + + md = metadata.Pairs( + sentry.SentryTraceHeader, span.ToSentryTrace(), + sentry.SentryBaggageHeader, span.ToBaggage(), + ) + return metadata.NewOutgoingContext(ctx, md) } diff --git a/grpc/client_test.go b/grpc/client_test.go new file mode 100644 index 000000000..404145ede --- /dev/null +++ b/grpc/client_test.go @@ -0,0 +1,235 @@ +package sentrygrpc_test + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/getsentry/sentry-go" + sentrygrpc "github.com/getsentry/sentry-go/grpc" + "github.com/stretchr/testify/assert" + "google.golang.org/grpc" + "google.golang.org/grpc/metadata" +) + +const defaultClientOperationName = "grpc.client" + +func TestClientOptions_SetDefaults(t *testing.T) { + t.Parallel() + + tests := map[string]struct { + options sentrygrpc.ClientOptions + assertions func(t *testing.T, options sentrygrpc.ClientOptions) + }{ + "Defaults are set when fields are empty": { + options: sentrygrpc.ClientOptions{}, + assertions: func(t *testing.T, options sentrygrpc.ClientOptions) { + assert.NotNil(t, options.ReportOn, "ReportOn should be set to default function") + assert.Equal(t, defaultClientOperationName, options.OperationName, "OperationName should be set to default value") + }, + }, + "Custom ReportOn is preserved": { + options: sentrygrpc.ClientOptions{ + ReportOn: func(err error) bool { + return err.Error() == "custom error" + }, + }, + assertions: func(t *testing.T, options sentrygrpc.ClientOptions) { + assert.NotNil(t, options.ReportOn, "ReportOn should not be nil") + err := errors.New("random error") + assert.False(t, options.ReportOn(err), "ReportOn should return false for random error") + }, + }, + "Custom OperationName is preserved": { + options: sentrygrpc.ClientOptions{ + OperationName: "custom.operation", + }, + assertions: func(t *testing.T, options sentrygrpc.ClientOptions) { + assert.Equal(t, "custom.operation", options.OperationName, "OperationName should be set to custom value") + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + test.options.SetDefaults() + test.assertions(t, test.options) + }) + } +} + +func TestUnaryClientInterceptor(t *testing.T) { + tests := map[string]struct { + invoker grpc.UnaryInvoker + options sentrygrpc.ClientOptions + expectedErr error + assertions func(t *testing.T, transport *sentry.TransportMock) + }{ + "Default behavior, no error": { + invoker: func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, opts ...grpc.CallOption) error { + return nil + }, + options: sentrygrpc.ClientOptions{}, + assertions: func(t *testing.T, transport *sentry.TransportMock) { + assert.Empty(t, transport.Events(), "No events should be captured") + }, + }, + "Error is reported": { + invoker: func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, opts ...grpc.CallOption) error { + return errors.New("test error") + }, + options: sentrygrpc.ClientOptions{}, + expectedErr: errors.New("test error"), + assertions: func(t *testing.T, transport *sentry.TransportMock) { + events := transport.Events() + assert.Len(t, events, 1, "One event should be captured") + assert.Equal(t, "test error", events[0].Exception[0].Value, "Captured exception should match the error") + }, + }, + "Metadata propagation": { + invoker: func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, opts ...grpc.CallOption) error { + md, ok := metadata.FromOutgoingContext(ctx) + assert.True(t, ok, "Metadata should be present in the outgoing context") + assert.Contains(t, md, sentry.SentryTraceHeader, "Metadata should contain Sentry trace header") + assert.Contains(t, md, sentry.SentryBaggageHeader, "Metadata should contain Sentry baggage header") + return nil + }, + options: sentrygrpc.ClientOptions{}, + assertions: func(t *testing.T, transport *sentry.TransportMock) {}, + }, + "Custom ReportOn behavior": { + invoker: func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, opts ...grpc.CallOption) error { + return errors.New("test error") + }, + options: sentrygrpc.ClientOptions{ + ReportOn: func(err error) bool { + return err.Error() == "specific error" + }, + }, + expectedErr: errors.New("test error"), + assertions: func(t *testing.T, transport *sentry.TransportMock) { + assert.Empty(t, transport.Events(), "No events should be captured due to custom ReportOn") + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + transport := &sentry.TransportMock{} + sentry.Init(sentry.ClientOptions{ + Transport: transport, + }) + + interceptor := sentrygrpc.UnaryClientInterceptor(test.options) + + // Execute the interceptor + err := interceptor(context.Background(), "/test.Service/TestMethod", struct{}{}, struct{}{}, nil, test.invoker) + + if test.expectedErr != nil { + assert.Equal(t, test.expectedErr, err, "Expected error mismatch") + } else { + assert.NoError(t, err, "Expected no error") + } + + sentry.Flush(2 * time.Second) + + // Pass the transport to the assertions to verify captured events. + test.assertions(t, transport) + }) + } +} + +func TestStreamClientInterceptor(t *testing.T) { + tests := map[string]struct { + streamer grpc.Streamer + options sentrygrpc.ClientOptions + expectedErr error + assertions func(t *testing.T, transport *sentry.TransportMock) + streamDesc *grpc.StreamDesc + }{ + "Default behavior, no error": { + streamer: func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, opts ...grpc.CallOption) (grpc.ClientStream, error) { + return nil, nil + }, + options: sentrygrpc.ClientOptions{}, + streamDesc: &grpc.StreamDesc{ + ClientStreams: true, + }, + assertions: func(t *testing.T, transport *sentry.TransportMock) { + assert.Empty(t, transport.Events(), "No events should be captured") + }, + }, + "Error is reported": { + streamer: func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, opts ...grpc.CallOption) (grpc.ClientStream, error) { + return nil, errors.New("test stream error") + }, + options: sentrygrpc.ClientOptions{}, + expectedErr: errors.New("test stream error"), + streamDesc: &grpc.StreamDesc{}, + assertions: func(t *testing.T, transport *sentry.TransportMock) { + events := transport.Events() + assert.Len(t, events, 1, "One event should be captured") + assert.Equal(t, "test stream error", events[0].Exception[0].Value, "Captured exception should match the error") + }, + }, + "Metadata propagation": { + streamer: func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, opts ...grpc.CallOption) (grpc.ClientStream, error) { + md, ok := metadata.FromOutgoingContext(ctx) + assert.True(t, ok, "Metadata should be present in the outgoing context") + assert.Contains(t, md, sentry.SentryTraceHeader, "Metadata should contain Sentry trace header") + assert.Contains(t, md, sentry.SentryBaggageHeader, "Metadata should contain Sentry baggage header") + return nil, nil + }, + options: sentrygrpc.ClientOptions{}, + streamDesc: &grpc.StreamDesc{ + ClientStreams: true, + }, + assertions: func(t *testing.T, transport *sentry.TransportMock) {}, + }, + "Custom ReportOn behavior": { + streamer: func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, opts ...grpc.CallOption) (grpc.ClientStream, error) { + return nil, errors.New("test stream error") + }, + options: sentrygrpc.ClientOptions{ + ReportOn: func(err error) bool { + return err.Error() == "specific error" + }, + }, + expectedErr: errors.New("test stream error"), + streamDesc: &grpc.StreamDesc{}, + assertions: func(t *testing.T, transport *sentry.TransportMock) { + assert.Empty(t, transport.Events(), "No events should be captured due to custom ReportOn") + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + // Reinitialize the transport for each test to ensure isolation. + transport := &sentry.TransportMock{} + sentry.Init(sentry.ClientOptions{ + Transport: transport, + }) + + interceptor := sentrygrpc.StreamClientInterceptor(test.options) + + // Execute the interceptor + clientStream, err := interceptor(context.Background(), test.streamDesc, nil, "/test.Service/TestMethod", test.streamer) + + if test.expectedErr != nil { + assert.Equal(t, test.expectedErr, err, "Expected error mismatch") + } else { + assert.NoError(t, err, "Expected no error") + } + + sentry.Flush(2 * time.Second) + + assert.Nil(t, clientStream, "ClientStream should be nil in this test scenario") + // Pass the transport to the assertions to verify captured events. + test.assertions(t, transport) + }) + } +} diff --git a/grpc/go.mod b/grpc/go.mod index 72975cf25..879cf6eb0 100644 --- a/grpc/go.mod +++ b/grpc/go.mod @@ -6,13 +6,18 @@ replace github.com/getsentry/sentry-go => ../ require ( github.com/getsentry/sentry-go v0.30.0 + github.com/stretchr/testify v1.8.2 google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 google.golang.org/grpc v1.69.2 ) require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect golang.org/x/net v0.30.0 // indirect golang.org/x/sys v0.26.0 // indirect golang.org/x/text v0.19.0 // indirect google.golang.org/protobuf v1.35.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/grpc/go.sum b/grpc/go.sum index 162aadade..c27b84b3f 100644 --- a/grpc/go.sum +++ b/grpc/go.sum @@ -1,3 +1,5 @@ +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= @@ -12,12 +14,23 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +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/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY= @@ -44,5 +57,9 @@ google.golang.org/grpc v1.69.2 h1:U3S9QEtbXC0bYNvRtcoklF3xGtLViumSYxWykJS+7AU= google.golang.org/grpc v1.69.2/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +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/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/grpc/server.go b/grpc/server.go index 343ce7058..e8d9dc0d4 100644 --- a/grpc/server.go +++ b/grpc/server.go @@ -69,7 +69,7 @@ func recoverWithSentry(ctx context.Context, hub *sentry.Hub, o ServerOptions) { } } -func reportErrorToSentry(hub *sentry.Hub, err error, methodName string, req interface{}, md map[string]string) { +func reportErrorToSentry(hub *sentry.Hub, err error, methodName string, req any, md map[string]string) { hub.WithScope(func(scope *sentry.Scope) { scope.SetExtras(map[string]any{ "grpc.method": methodName, @@ -111,7 +111,7 @@ func reportErrorToSentry(hub *sentry.Hub, err error, methodName string, req inte func UnaryServerInterceptor(opts ServerOptions) grpc.UnaryServerInterceptor { opts.SetDefaults() - return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { + return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { hub := sentry.GetHubFromContext(ctx) if hub == nil { hub = sentry.CurrentHub().Clone() @@ -135,7 +135,7 @@ func UnaryServerInterceptor(opts ServerOptions) grpc.UnaryServerInterceptor { options := []sentry.SpanOption{ sentry.ContinueTrace(hub, sentryTraceHeader, sentryBaggageHeader), - sentry.WithOpName("http.server"), + sentry.WithOpName(opts.OperationName), sentry.WithDescription(info.FullMethod), sentry.WithTransactionSource(sentry.SourceURL), } @@ -176,7 +176,7 @@ func UnaryServerInterceptor(opts ServerOptions) grpc.UnaryServerInterceptor { // StreamServerInterceptor provides Sentry integration for streaming gRPC calls. func StreamServerInterceptor(opts ServerOptions) grpc.StreamServerInterceptor { opts.SetDefaults() - return func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { + return func(srv any, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { ctx := ss.Context() hub := sentry.GetHubFromContext(ctx) if hub == nil { @@ -201,7 +201,7 @@ func StreamServerInterceptor(opts ServerOptions) grpc.StreamServerInterceptor { options := []sentry.SpanOption{ sentry.ContinueTrace(hub, sentryTraceHeader, sentryBaggageHeader), - sentry.WithOpName("http.server"), + sentry.WithOpName(opts.OperationName), sentry.WithDescription(info.FullMethod), sentry.WithTransactionSource(sentry.SourceURL), } diff --git a/grpc/server_test.go b/grpc/server_test.go new file mode 100644 index 000000000..933b865e2 --- /dev/null +++ b/grpc/server_test.go @@ -0,0 +1,362 @@ +package sentrygrpc_test + +import ( + "context" + "errors" + "fmt" + "testing" + "time" + + "github.com/getsentry/sentry-go" + sentrygrpc "github.com/getsentry/sentry-go/grpc" + "github.com/stretchr/testify/assert" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" +) + +const defaultServerOperationName = "grpc.server" + +func TestServerOptions_SetDefaults(t *testing.T) { + t.Parallel() + tests := map[string]struct { + options sentrygrpc.ServerOptions + assertions func(t *testing.T, options sentrygrpc.ServerOptions) + }{ + "Defaults are set when fields are empty": { + options: sentrygrpc.ServerOptions{}, + assertions: func(t *testing.T, options sentrygrpc.ServerOptions) { + assert.NotNil(t, options.ReportOn, "ReportOn should be set to default function") + assert.Equal(t, sentry.DefaultFlushTimeout, options.Timeout, "Timeout should be set to default value") + assert.Equal(t, defaultServerOperationName, options.OperationName, "OperationName should be set to default value") + }, + }, + "Custom ReportOn is preserved": { + options: sentrygrpc.ServerOptions{ + ReportOn: func(err error) bool { + return err.Error() == "specific error" + }, + }, + assertions: func(t *testing.T, options sentrygrpc.ServerOptions) { + assert.NotNil(t, options.ReportOn, "ReportOn should not be nil") + err := errors.New("random error") + assert.False(t, options.ReportOn(err), "ReportOn should return false for random error") + err = errors.New("specific error") + assert.True(t, options.ReportOn(err), "ReportOn should return true for specific error") + }, + }, + "Custom Timeout is preserved": { + options: sentrygrpc.ServerOptions{ + Timeout: 5 * time.Second, + }, + assertions: func(t *testing.T, options sentrygrpc.ServerOptions) { + assert.Equal(t, 5*time.Second, options.Timeout, "Timeout should be set to custom value") + }, + }, + "Custom OperationName is preserved": { + options: sentrygrpc.ServerOptions{ + OperationName: "custom.operation", + }, + assertions: func(t *testing.T, options sentrygrpc.ServerOptions) { + assert.Equal(t, "custom.operation", options.OperationName, "OperationName should be set to custom value") + }, + }, + "CaptureRequestBody remains unchanged": { + options: sentrygrpc.ServerOptions{ + CaptureRequestBody: true, + }, + assertions: func(t *testing.T, options sentrygrpc.ServerOptions) { + assert.True(t, options.CaptureRequestBody, "CaptureRequestBody should remain true") + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + test.options.SetDefaults() + + test.assertions(t, test.options) + }) + } +} + +func TestUnaryServerInterceptor(t *testing.T) { + tests := map[string]struct { + options sentrygrpc.ServerOptions + handler grpc.UnaryHandler + expectedErr string + wantException string + wantTransaction *sentry.Event + assertTransaction bool + }{ + "Handle panic and re-panic": { + options: sentrygrpc.ServerOptions{Repanic: true}, + handler: func(ctx context.Context, req any) (any, error) { + panic("test panic") + }, + }, + "Report error with transaction": { + options: sentrygrpc.ServerOptions{ + ReportOn: func(err error) bool { + return true + }, + }, + handler: func(ctx context.Context, req any) (any, error) { + return nil, status.Error(codes.Internal, "handler error") + }, + expectedErr: "rpc error: code = Internal desc = handler error", + wantException: "rpc error: code = Internal desc = handler error", + assertTransaction: true, + }, + "Do not report error when ReportOn returns false": { + options: sentrygrpc.ServerOptions{ + ReportOn: func(err error) bool { + return false + }, + }, + handler: func(ctx context.Context, req any) (any, error) { + return nil, status.Error(codes.Internal, "handler error not reported") + }, + expectedErr: "rpc error: code = Internal desc = handler error not reported", + assertTransaction: true, + }, + "Capture request body when enabled": { + options: sentrygrpc.ServerOptions{ + CaptureRequestBody: true, + ReportOn: func(err error) bool { + return true + }, + }, + handler: func(ctx context.Context, req any) (any, error) { + return nil, status.Error(codes.InvalidArgument, "invalid request body") + }, + expectedErr: "rpc error: code = InvalidArgument desc = invalid request body", + wantException: "rpc error: code = InvalidArgument desc = invalid request body", + assertTransaction: true, + }, + "Custom operation name is used in transaction": { + options: sentrygrpc.ServerOptions{ + OperationName: "CustomUnaryOperation", + ReportOn: func(err error) bool { + return true + }, + }, + handler: func(ctx context.Context, req any) (any, error) { + return nil, status.Error(codes.PermissionDenied, "access denied") + }, + expectedErr: "rpc error: code = PermissionDenied desc = access denied", + wantException: "rpc error: code = PermissionDenied desc = access denied", + assertTransaction: true, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + eventsCh := make(chan *sentry.Event, 1) + transactionsCh := make(chan *sentry.Event, 1) + + err := sentry.Init(sentry.ClientOptions{ + BeforeSend: func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event { + eventsCh <- event + return event + }, + BeforeSendTransaction: func(tx *sentry.Event, hint *sentry.EventHint) *sentry.Event { + fmt.Println("Transaction: ", tx.Transaction) + transactionsCh <- tx + return tx + }, + EnableTracing: true, + TracesSampleRate: 1.0, + }) + if err != nil { + t.Fatal(err) + } + + interceptor := sentrygrpc.UnaryServerInterceptor(test.options) + + defer func() { + if r := recover(); r != nil { + // Assert the panic message for tests with repanic enabled + if test.options.Repanic { + assert.Equal(t, "test panic", r, "Expected panic to propagate with message 'test panic'") + } + } + }() + + _, err = interceptor(context.Background(), nil, &grpc.UnaryServerInfo{ + FullMethod: "TestService.Method", + }, test.handler) + + if test.expectedErr != "" { + assert.EqualError(t, err, test.expectedErr) + } else { + assert.NoError(t, err) + } + + if test.wantException != "" { + close(eventsCh) + var gotEvent *sentry.Event + for e := range eventsCh { + gotEvent = e + } + + assert.NotNil(t, gotEvent, "Expected an event") + assert.Len(t, gotEvent.Exception, 1, "Expected one exception in the event") + assert.Equal(t, test.wantException, gotEvent.Exception[0].Value, "Exception values should match") + } + + if test.assertTransaction { + close(transactionsCh) + var gotTransaction *sentry.Event + for tx := range transactionsCh { + fmt.Println("Transaction: ", tx.Transaction) + gotTransaction = tx + } + assert.NotNil(t, gotTransaction, "Expected a transaction") + assert.Equal(t, fmt.Sprintf("UnaryServerInterceptor %s", "TestService.Method"), gotTransaction.Transaction, "Transaction names should match") + } + + sentry.Flush(2 * time.Second) + }) + } +} + +// wrappedServerStream is a wrapper around grpc.ServerStream that overrides the Context method. +type wrappedServerStream struct { + grpc.ServerStream + ctx context.Context +} + +// Context returns the custom context for the stream. +func (w *wrappedServerStream) Context() context.Context { + return w.ctx +} + +func TestStreamServerInterceptor(t *testing.T) { + t.Parallel() + tests := map[string]struct { + options sentrygrpc.ServerOptions + handler grpc.StreamHandler + expectedErr string + expectedMetadata bool + expectedEvent bool + }{ + "Default behavior, no error": { + options: sentrygrpc.ServerOptions{}, + handler: func(srv any, stream grpc.ServerStream) error { + return nil + }, + expectedErr: "", + expectedMetadata: false, + expectedEvent: false, + }, + "Handler returns an error": { + options: sentrygrpc.ServerOptions{ + ReportOn: func(err error) bool { + return true + }, + }, + handler: func(srv any, stream grpc.ServerStream) error { + return status.Error(codes.Internal, "stream error") + }, + expectedErr: "rpc error: code = Internal desc = stream error", + expectedMetadata: false, + expectedEvent: true, + }, + "Repanic is enabled": { + options: sentrygrpc.ServerOptions{ + Repanic: true, + }, + handler: func(srv any, stream grpc.ServerStream) error { + panic("test panic") + }, + expectedErr: "", + expectedMetadata: false, + expectedEvent: false, // The panic is re-raised, event capture may depend on hub state + }, + "Metadata is propagated": { + options: sentrygrpc.ServerOptions{}, + handler: func(srv any, stream grpc.ServerStream) error { + md, ok := metadata.FromIncomingContext(stream.Context()) + if !ok || len(md) == 0 { + return status.Error(codes.InvalidArgument, "metadata missing") + } + return nil + }, + expectedErr: "", + expectedMetadata: true, + expectedEvent: false, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + eventsCh := make(chan *sentry.Event, 1) + transactionsCh := make(chan *sentry.Event, 1) + + err := sentry.Init(sentry.ClientOptions{ + BeforeSend: func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event { + eventsCh <- event + return event + }, + BeforeSendTransaction: func(tx *sentry.Event, hint *sentry.EventHint) *sentry.Event { + transactionsCh <- tx + return tx + }, + }) + if err != nil { + t.Fatal(err) + } + defer sentry.Flush(2 * time.Second) + + interceptor := sentrygrpc.StreamServerInterceptor(test.options) + + // Simulate a server stream + stream := &wrappedServerStream{ + ServerStream: nil, // Mock implementation or use a testing framework + ctx: metadata.NewIncomingContext(context.Background(), metadata.Pairs("key", "value")), + } + + var recovered interface{} + func() { + defer func() { + recovered = recover() + }() + err = interceptor(nil, stream, &grpc.StreamServerInfo{FullMethod: "TestService.StreamMethod"}, test.handler) + }() + + if test.expectedErr != "" { + assert.EqualError(t, err, test.expectedErr) + } else { + assert.NoError(t, err) + } + + if test.expectedMetadata { + md, ok := metadata.FromIncomingContext(stream.Context()) + assert.True(t, ok, "Expected metadata to be propagated in context") + assert.Contains(t, md, "key", "Expected metadata to include 'key'") + } + + if test.expectedEvent { + close(eventsCh) + var gotEvent *sentry.Event + for e := range eventsCh { + gotEvent = e + } + assert.NotNil(t, gotEvent, "Expected an event to be captured") + } else { + assert.Empty(t, eventsCh, "Expected no event to be captured") + } + + if test.options.Repanic { + assert.NotNil(t, recovered, "Expected panic to be re-raised") + assert.Equal(t, "test panic", recovered, "Panic value should match") + } + }) + } +} diff --git a/mocks_test.go b/mocks.go similarity index 99% rename from mocks_test.go rename to mocks.go index 5cc127e1b..61731464d 100644 --- a/mocks_test.go +++ b/mocks.go @@ -43,4 +43,3 @@ func (t *TransportMock) Events() []*Event { return t.events } func (t *TransportMock) Close() {} - diff --git a/otel/event_processor_test.go b/otel/event_processor_test.go index 43df64a9a..28116d27b 100644 --- a/otel/event_processor_test.go +++ b/otel/event_processor_test.go @@ -34,7 +34,7 @@ func TestLinkTraceContextToErrorEventSetsContext(t *testing.T) { hub.Scope(), ) - transport := client.Transport.(*TransportMock) + transport := client.Transport.(*sentry.TransportMock) events := transport.Events() assertEqual(t, len(events), 1) err := events[0] diff --git a/otel/helpers_test.go b/otel/helpers_test.go index 227f1a804..09f648c58 100644 --- a/otel/helpers_test.go +++ b/otel/helpers_test.go @@ -3,9 +3,7 @@ package sentryotel import ( "encoding/hex" "sort" - "sync" "testing" - "time" "github.com/getsentry/sentry-go" "github.com/getsentry/sentry-go/internal/otel/baggage" @@ -112,37 +110,3 @@ func otelSpanIDFromHex(s string) trace.SpanID { } return spanID } - -// FIXME(anton): TransportMock is copied from mocks_test.go -// I don't see an easy way right now to reuse this struct in "sentry" and -// "sentryotel" packages: it naturally depends on "sentry", but tests in "sentry" -// package also depend on it, so if we move it to a new package, we'll get an -// import cycle. -// Alternatively, it could be made public on "sentry" package, but it doesn't -// feel right. - -type TransportMock struct { - mu sync.Mutex - events []*sentry.Event - lastEvent *sentry.Event -} - -func (t *TransportMock) Configure(options sentry.ClientOptions) {} -func (t *TransportMock) SendEvent(event *sentry.Event) { - t.mu.Lock() - defer t.mu.Unlock() - t.events = append(t.events, event) - t.lastEvent = event -} -func (t *TransportMock) Flush(timeout time.Duration) bool { - return true -} -func (t *TransportMock) Events() []*sentry.Event { - t.mu.Lock() - defer t.mu.Unlock() - return t.events -} - -func (t *TransportMock) Close() {} - -// diff --git a/otel/span_processor_test.go b/otel/span_processor_test.go index 03c9796cd..ecd4a0e2d 100644 --- a/otel/span_processor_test.go +++ b/otel/span_processor_test.go @@ -40,15 +40,15 @@ func emptyContextWithSentry() context.Context { Release: "1.2.3", EnableTracing: true, TracesSampleRate: 1.0, - Transport: &TransportMock{}, + Transport: &sentry.TransportMock{}, }) hub := sentry.NewHub(client, sentry.NewScope()) return sentry.SetHubOnContext(context.Background(), hub) } -func getSentryTransportFromContext(ctx context.Context) *TransportMock { +func getSentryTransportFromContext(ctx context.Context) *sentry.TransportMock { hub := sentry.GetHubFromContext(ctx) - transport, ok := hub.Client().Transport.(*TransportMock) + transport, ok := hub.Client().Transport.(*sentry.TransportMock) if !ok { log.Fatal( "Cannot get mock transport from context", From a904dfda02fc9193fa4fdfab494ffb0c5e9f4aa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emir=20Ribi=C4=87?= Date: Fri, 27 Dec 2024 09:57:35 +0100 Subject: [PATCH 4/9] update grpc dependencies --- _examples/grpc_client/main.go | 116 ----------- _examples/grpc_server/example.proto | 21 -- _examples/grpc_server/examplepb/example.pb.go | 191 ------------------ .../grpc_server/examplepb/example_grpc.pb.go | 158 --------------- _examples/grpc_server/main.go | 95 --------- grpc/go.mod | 10 +- grpc/go.sum | 20 +- 7 files changed, 15 insertions(+), 596 deletions(-) delete mode 100644 _examples/grpc_client/main.go delete mode 100644 _examples/grpc_server/example.proto delete mode 100644 _examples/grpc_server/examplepb/example.pb.go delete mode 100644 _examples/grpc_server/examplepb/example_grpc.pb.go delete mode 100644 _examples/grpc_server/main.go diff --git a/_examples/grpc_client/main.go b/_examples/grpc_client/main.go deleted file mode 100644 index 144ad59d2..000000000 --- a/_examples/grpc_client/main.go +++ /dev/null @@ -1,116 +0,0 @@ -package main - -import ( - "context" - "fmt" - "log" - "time" - - "grpcdemo/cmd/server/examplepb" - - "github.com/getsentry/sentry-go" - sentrygrpc "github.com/getsentry/sentry-go/grpc" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials/insecure" - "google.golang.org/grpc/metadata" -) - -const grpcServerAddress = "localhost:50051" - -func main() { - // Initialize Sentry - err := sentry.Init(sentry.ClientOptions{ - Dsn: "", - TracesSampleRate: 1.0, - }) - if err != nil { - log.Fatalf("sentry.Init: %s", err) - } - defer sentry.Flush(2 * time.Second) - - // Create a connection to the gRPC server with Sentry interceptors - conn, err := grpc.NewClient( - grpcServerAddress, - grpc.WithTransportCredentials(insecure.NewCredentials()), // Use TLS in production - grpc.WithUnaryInterceptor(sentrygrpc.UnaryClientInterceptor(sentrygrpc.ClientOptions{})), - grpc.WithStreamInterceptor(sentrygrpc.StreamClientInterceptor(sentrygrpc.ClientOptions{})), - ) - if err != nil { - log.Fatalf("Failed to connect to gRPC server: %s", err) - } - defer conn.Close() - - // Create a client for the ExampleService - client := examplepb.NewExampleServiceClient(conn) - - // Perform Unary call - fmt.Println("Performing Unary Call:") - unaryExample(client) - - // Perform Streaming call - fmt.Println("\nPerforming Streaming Call:") - streamExample(client) -} - -func unaryExample(client examplepb.ExampleServiceClient) { - ctx := context.Background() - - // Add metadata to the context - ctx = metadata.NewOutgoingContext(ctx, metadata.Pairs( - "custom-header", "value", - )) - - req := &examplepb.ExampleRequest{ - Message: "Hello, server!", // Change to "error" to simulate an error - } - - res, err := client.UnaryExample(ctx, req) - if err != nil { - fmt.Printf("Unary Call Error: %v\n", err) - sentry.CaptureException(err) - return - } - - fmt.Printf("Unary Response: %s\n", res.Message) -} - -func streamExample(client examplepb.ExampleServiceClient) { - ctx := context.Background() - - // Add metadata to the context - ctx = metadata.NewOutgoingContext(ctx, metadata.Pairs( - "streaming-header", "stream-value", - )) - - stream, err := client.StreamExample(ctx) - if err != nil { - fmt.Printf("Failed to establish stream: %v\n", err) - sentry.CaptureException(err) - return - } - - // Send multiple messages in the stream - messages := []string{"Message 1", "Message 2", "error", "Message 4"} - for _, msg := range messages { - err := stream.Send(&examplepb.ExampleRequest{Message: msg}) - if err != nil { - fmt.Printf("Stream Send Error: %v\n", err) - sentry.CaptureException(err) - return - } - } - - // Close the stream for sending - stream.CloseSend() - - // Receive responses from the server - for { - res, err := stream.Recv() - if err != nil { - fmt.Printf("Stream Recv Error: %v\n", err) - sentry.CaptureException(err) - break - } - fmt.Printf("Stream Response: %s\n", res.Message) - } -} diff --git a/_examples/grpc_server/example.proto b/_examples/grpc_server/example.proto deleted file mode 100644 index 356d58f11..000000000 --- a/_examples/grpc_server/example.proto +++ /dev/null @@ -1,21 +0,0 @@ -syntax = "proto3"; - -package main; - -option go_package = "github.com/your-username/your-repo/examplepb;examplepb"; - -// ExampleService defines the gRPC service. -service ExampleService { - rpc UnaryExample(ExampleRequest) returns (ExampleResponse); - rpc StreamExample(stream ExampleRequest) returns (stream ExampleResponse); -} - -// ExampleRequest is the request message. -message ExampleRequest { - string message = 1; -} - -// ExampleResponse is the response message. -message ExampleResponse { - string message = 1; -} diff --git a/_examples/grpc_server/examplepb/example.pb.go b/_examples/grpc_server/examplepb/example.pb.go deleted file mode 100644 index 84d8b8fbb..000000000 --- a/_examples/grpc_server/examplepb/example.pb.go +++ /dev/null @@ -1,191 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.36.1 -// protoc v5.29.2 -// source: example.proto - -package examplepb - -import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - reflect "reflect" - sync "sync" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -// ExampleRequest is the request message. -type ExampleRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ExampleRequest) Reset() { - *x = ExampleRequest{} - mi := &file_example_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ExampleRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ExampleRequest) ProtoMessage() {} - -func (x *ExampleRequest) ProtoReflect() protoreflect.Message { - mi := &file_example_proto_msgTypes[0] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ExampleRequest.ProtoReflect.Descriptor instead. -func (*ExampleRequest) Descriptor() ([]byte, []int) { - return file_example_proto_rawDescGZIP(), []int{0} -} - -func (x *ExampleRequest) GetMessage() string { - if x != nil { - return x.Message - } - return "" -} - -// ExampleResponse is the response message. -type ExampleResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ExampleResponse) Reset() { - *x = ExampleResponse{} - mi := &file_example_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ExampleResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ExampleResponse) ProtoMessage() {} - -func (x *ExampleResponse) ProtoReflect() protoreflect.Message { - mi := &file_example_proto_msgTypes[1] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ExampleResponse.ProtoReflect.Descriptor instead. -func (*ExampleResponse) Descriptor() ([]byte, []int) { - return file_example_proto_rawDescGZIP(), []int{1} -} - -func (x *ExampleResponse) GetMessage() string { - if x != nil { - return x.Message - } - return "" -} - -var File_example_proto protoreflect.FileDescriptor - -var file_example_proto_rawDesc = []byte{ - 0x0a, 0x0d, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, - 0x04, 0x6d, 0x61, 0x69, 0x6e, 0x22, 0x2a, 0x0a, 0x0e, 0x45, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, - 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, - 0x65, 0x22, 0x2b, 0x0a, 0x0f, 0x45, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x32, 0x8f, - 0x01, 0x0a, 0x0e, 0x45, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, - 0x65, 0x12, 0x3b, 0x0a, 0x0c, 0x55, 0x6e, 0x61, 0x72, 0x79, 0x45, 0x78, 0x61, 0x6d, 0x70, 0x6c, - 0x65, 0x12, 0x14, 0x2e, 0x6d, 0x61, 0x69, 0x6e, 0x2e, 0x45, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x6d, 0x61, 0x69, 0x6e, 0x2e, 0x45, - 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x40, - 0x0a, 0x0d, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x45, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x12, - 0x14, 0x2e, 0x6d, 0x61, 0x69, 0x6e, 0x2e, 0x45, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x6d, 0x61, 0x69, 0x6e, 0x2e, 0x45, 0x78, 0x61, - 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x28, 0x01, 0x30, 0x01, - 0x42, 0x38, 0x5a, 0x36, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x79, - 0x6f, 0x75, 0x72, 0x2d, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x2f, 0x79, 0x6f, 0x75, - 0x72, 0x2d, 0x72, 0x65, 0x70, 0x6f, 0x2f, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x70, 0x62, - 0x3b, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x33, -} - -var ( - file_example_proto_rawDescOnce sync.Once - file_example_proto_rawDescData = file_example_proto_rawDesc -) - -func file_example_proto_rawDescGZIP() []byte { - file_example_proto_rawDescOnce.Do(func() { - file_example_proto_rawDescData = protoimpl.X.CompressGZIP(file_example_proto_rawDescData) - }) - return file_example_proto_rawDescData -} - -var file_example_proto_msgTypes = make([]protoimpl.MessageInfo, 2) -var file_example_proto_goTypes = []any{ - (*ExampleRequest)(nil), // 0: main.ExampleRequest - (*ExampleResponse)(nil), // 1: main.ExampleResponse -} -var file_example_proto_depIdxs = []int32{ - 0, // 0: main.ExampleService.UnaryExample:input_type -> main.ExampleRequest - 0, // 1: main.ExampleService.StreamExample:input_type -> main.ExampleRequest - 1, // 2: main.ExampleService.UnaryExample:output_type -> main.ExampleResponse - 1, // 3: main.ExampleService.StreamExample:output_type -> main.ExampleResponse - 2, // [2:4] is the sub-list for method output_type - 0, // [0:2] is the sub-list for method input_type - 0, // [0:0] is the sub-list for extension type_name - 0, // [0:0] is the sub-list for extension extendee - 0, // [0:0] is the sub-list for field type_name -} - -func init() { file_example_proto_init() } -func file_example_proto_init() { - if File_example_proto != nil { - return - } - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_example_proto_rawDesc, - NumEnums: 0, - NumMessages: 2, - NumExtensions: 0, - NumServices: 1, - }, - GoTypes: file_example_proto_goTypes, - DependencyIndexes: file_example_proto_depIdxs, - MessageInfos: file_example_proto_msgTypes, - }.Build() - File_example_proto = out.File - file_example_proto_rawDesc = nil - file_example_proto_goTypes = nil - file_example_proto_depIdxs = nil -} diff --git a/_examples/grpc_server/examplepb/example_grpc.pb.go b/_examples/grpc_server/examplepb/example_grpc.pb.go deleted file mode 100644 index 56f4b3504..000000000 --- a/_examples/grpc_server/examplepb/example_grpc.pb.go +++ /dev/null @@ -1,158 +0,0 @@ -// Code generated by protoc-gen-go-grpc. DO NOT EDIT. -// versions: -// - protoc-gen-go-grpc v1.5.1 -// - protoc v5.29.2 -// source: example.proto - -package examplepb - -import ( - context "context" - grpc "google.golang.org/grpc" - codes "google.golang.org/grpc/codes" - status "google.golang.org/grpc/status" -) - -// This is a compile-time assertion to ensure that this generated file -// is compatible with the grpc package it is being compiled against. -// Requires gRPC-Go v1.64.0 or later. -const _ = grpc.SupportPackageIsVersion9 - -const ( - ExampleService_UnaryExample_FullMethodName = "/main.ExampleService/UnaryExample" - ExampleService_StreamExample_FullMethodName = "/main.ExampleService/StreamExample" -) - -// ExampleServiceClient is the client API for ExampleService service. -// -// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. -// -// ExampleService defines the gRPC service. -type ExampleServiceClient interface { - UnaryExample(ctx context.Context, in *ExampleRequest, opts ...grpc.CallOption) (*ExampleResponse, error) - StreamExample(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[ExampleRequest, ExampleResponse], error) -} - -type exampleServiceClient struct { - cc grpc.ClientConnInterface -} - -func NewExampleServiceClient(cc grpc.ClientConnInterface) ExampleServiceClient { - return &exampleServiceClient{cc} -} - -func (c *exampleServiceClient) UnaryExample(ctx context.Context, in *ExampleRequest, opts ...grpc.CallOption) (*ExampleResponse, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(ExampleResponse) - err := c.cc.Invoke(ctx, ExampleService_UnaryExample_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *exampleServiceClient) StreamExample(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[ExampleRequest, ExampleResponse], error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - stream, err := c.cc.NewStream(ctx, &ExampleService_ServiceDesc.Streams[0], ExampleService_StreamExample_FullMethodName, cOpts...) - if err != nil { - return nil, err - } - x := &grpc.GenericClientStream[ExampleRequest, ExampleResponse]{ClientStream: stream} - return x, nil -} - -// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. -type ExampleService_StreamExampleClient = grpc.BidiStreamingClient[ExampleRequest, ExampleResponse] - -// ExampleServiceServer is the server API for ExampleService service. -// All implementations must embed UnimplementedExampleServiceServer -// for forward compatibility. -// -// ExampleService defines the gRPC service. -type ExampleServiceServer interface { - UnaryExample(context.Context, *ExampleRequest) (*ExampleResponse, error) - StreamExample(grpc.BidiStreamingServer[ExampleRequest, ExampleResponse]) error - mustEmbedUnimplementedExampleServiceServer() -} - -// UnimplementedExampleServiceServer must be embedded to have -// forward compatible implementations. -// -// NOTE: this should be embedded by value instead of pointer to avoid a nil -// pointer dereference when methods are called. -type UnimplementedExampleServiceServer struct{} - -func (UnimplementedExampleServiceServer) UnaryExample(context.Context, *ExampleRequest) (*ExampleResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method UnaryExample not implemented") -} -func (UnimplementedExampleServiceServer) StreamExample(grpc.BidiStreamingServer[ExampleRequest, ExampleResponse]) error { - return status.Errorf(codes.Unimplemented, "method StreamExample not implemented") -} -func (UnimplementedExampleServiceServer) mustEmbedUnimplementedExampleServiceServer() {} -func (UnimplementedExampleServiceServer) testEmbeddedByValue() {} - -// UnsafeExampleServiceServer may be embedded to opt out of forward compatibility for this service. -// Use of this interface is not recommended, as added methods to ExampleServiceServer will -// result in compilation errors. -type UnsafeExampleServiceServer interface { - mustEmbedUnimplementedExampleServiceServer() -} - -func RegisterExampleServiceServer(s grpc.ServiceRegistrar, srv ExampleServiceServer) { - // If the following call pancis, it indicates UnimplementedExampleServiceServer was - // embedded by pointer and is nil. This will cause panics if an - // unimplemented method is ever invoked, so we test this at initialization - // time to prevent it from happening at runtime later due to I/O. - if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { - t.testEmbeddedByValue() - } - s.RegisterService(&ExampleService_ServiceDesc, srv) -} - -func _ExampleService_UnaryExample_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(ExampleRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(ExampleServiceServer).UnaryExample(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: ExampleService_UnaryExample_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(ExampleServiceServer).UnaryExample(ctx, req.(*ExampleRequest)) - } - return interceptor(ctx, in, info, handler) -} - -func _ExampleService_StreamExample_Handler(srv interface{}, stream grpc.ServerStream) error { - return srv.(ExampleServiceServer).StreamExample(&grpc.GenericServerStream[ExampleRequest, ExampleResponse]{ServerStream: stream}) -} - -// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. -type ExampleService_StreamExampleServer = grpc.BidiStreamingServer[ExampleRequest, ExampleResponse] - -// ExampleService_ServiceDesc is the grpc.ServiceDesc for ExampleService service. -// It's only intended for direct use with grpc.RegisterService, -// and not to be introspected or modified (even as a copy) -var ExampleService_ServiceDesc = grpc.ServiceDesc{ - ServiceName: "main.ExampleService", - HandlerType: (*ExampleServiceServer)(nil), - Methods: []grpc.MethodDesc{ - { - MethodName: "UnaryExample", - Handler: _ExampleService_UnaryExample_Handler, - }, - }, - Streams: []grpc.StreamDesc{ - { - StreamName: "StreamExample", - Handler: _ExampleService_StreamExample_Handler, - ServerStreams: true, - ClientStreams: true, - }, - }, - Metadata: "example.proto", -} diff --git a/_examples/grpc_server/main.go b/_examples/grpc_server/main.go deleted file mode 100644 index defe1b09a..000000000 --- a/_examples/grpc_server/main.go +++ /dev/null @@ -1,95 +0,0 @@ -package main - -import ( - "context" - "fmt" - "grpcdemo/cmd/server/examplepb" - "log" - "net" - "time" - - "github.com/getsentry/sentry-go" - sentrygrpc "github.com/getsentry/sentry-go/grpc" - "google.golang.org/grpc" - "google.golang.org/grpc/metadata" -) - -const grpcPort = ":50051" - -// ExampleServiceServer is the server implementation for the ExampleService. -type ExampleServiceServer struct { - examplepb.UnimplementedExampleServiceServer -} - -// UnaryExample handles unary gRPC requests. -func (s *ExampleServiceServer) UnaryExample(ctx context.Context, req *examplepb.ExampleRequest) (*examplepb.ExampleResponse, error) { - md, _ := metadata.FromIncomingContext(ctx) - fmt.Printf("Received Unary Request: %v\nMetadata: %v\n", req.Message, md) - - // Simulate an error for demonstration - if req.Message == "error" { - return nil, fmt.Errorf("simulated unary error") - } - - return &examplepb.ExampleResponse{Message: fmt.Sprintf("Hello, %s!", req.Message)}, nil -} - -// StreamExample handles bidirectional streaming gRPC requests. -func (s *ExampleServiceServer) StreamExample(stream examplepb.ExampleService_StreamExampleServer) error { - for { - req, err := stream.Recv() - if err != nil { - fmt.Printf("Stream Recv Error: %v\n", err) - return err - } - - fmt.Printf("Received Stream Message: %v\n", req.Message) - - if req.Message == "error" { - return fmt.Errorf("simulated stream error") - } - - err = stream.Send(&examplepb.ExampleResponse{Message: fmt.Sprintf("Echo: %s", req.Message)}) - if err != nil { - fmt.Printf("Stream Send Error: %v\n", err) - return err - } - } -} - -func main() { - // Initialize Sentry - err := sentry.Init(sentry.ClientOptions{ - Dsn: "", - TracesSampleRate: 1.0, - }) - if err != nil { - log.Fatalf("sentry.Init: %s", err) - } - defer sentry.Flush(2 * time.Second) - - // Create a new gRPC server with Sentry interceptors - server := grpc.NewServer( - grpc.UnaryInterceptor(sentrygrpc.UnaryServerInterceptor(sentrygrpc.ServerOptions{ - Repanic: true, - CaptureRequestBody: true, - })), - grpc.StreamInterceptor(sentrygrpc.StreamServerInterceptor(sentrygrpc.ServerOptions{ - Repanic: true, - })), - ) - - // Register the ExampleService - examplepb.RegisterExampleServiceServer(server, &ExampleServiceServer{}) - - // Start the server - listener, err := net.Listen("tcp", grpcPort) - if err != nil { - log.Fatalf("Failed to listen on port %s: %v", grpcPort, err) - } - - fmt.Printf("gRPC server is running on %s\n", grpcPort) - if err := server.Serve(listener); err != nil { - log.Fatalf("Failed to serve: %v", err) - } -} diff --git a/grpc/go.mod b/grpc/go.mod index 879cf6eb0..00114b6c2 100644 --- a/grpc/go.mod +++ b/grpc/go.mod @@ -7,7 +7,7 @@ replace github.com/getsentry/sentry-go => ../ require ( github.com/getsentry/sentry-go v0.30.0 github.com/stretchr/testify v1.8.2 - google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 + google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 google.golang.org/grpc v1.69.2 ) @@ -15,9 +15,9 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/kr/text v0.2.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/net v0.30.0 // indirect - golang.org/x/sys v0.26.0 // indirect - golang.org/x/text v0.19.0 // indirect - google.golang.org/protobuf v1.35.1 // indirect + golang.org/x/net v0.33.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/text v0.21.0 // indirect + google.golang.org/protobuf v1.36.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/grpc/go.sum b/grpc/go.sum index c27b84b3f..e43760af9 100644 --- a/grpc/go.sum +++ b/grpc/go.sum @@ -45,18 +45,18 @@ go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HY go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= -golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= -golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= -golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= -golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 h1:X58yt85/IXCx0Y3ZwN6sEIKZzQtDEYaBWrDvErdXrRE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 h1:TqExAhdPaB60Ux47Cn0oLV07rGnxZzIsaRhQaqS666A= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA= google.golang.org/grpc v1.69.2 h1:U3S9QEtbXC0bYNvRtcoklF3xGtLViumSYxWykJS+7AU= google.golang.org/grpc v1.69.2/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= -google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= -google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= +google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 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/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= From 2f3c7c59c5ce7f71ad0cbfdd21659ae7ac86faa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emir=20Ribi=C4=87?= Date: Fri, 27 Dec 2024 09:58:17 +0100 Subject: [PATCH 5/9] Update grpc example structure --- _examples/grpc/client/main.go | 116 +++++++++++ _examples/grpc/server/example.proto | 21 ++ _examples/grpc/server/examplepb/example.pb.go | 191 ++++++++++++++++++ .../grpc/server/examplepb/example_grpc.pb.go | 158 +++++++++++++++ _examples/grpc/server/main.go | 95 +++++++++ 5 files changed, 581 insertions(+) create mode 100644 _examples/grpc/client/main.go create mode 100644 _examples/grpc/server/example.proto create mode 100644 _examples/grpc/server/examplepb/example.pb.go create mode 100644 _examples/grpc/server/examplepb/example_grpc.pb.go create mode 100644 _examples/grpc/server/main.go diff --git a/_examples/grpc/client/main.go b/_examples/grpc/client/main.go new file mode 100644 index 000000000..144ad59d2 --- /dev/null +++ b/_examples/grpc/client/main.go @@ -0,0 +1,116 @@ +package main + +import ( + "context" + "fmt" + "log" + "time" + + "grpcdemo/cmd/server/examplepb" + + "github.com/getsentry/sentry-go" + sentrygrpc "github.com/getsentry/sentry-go/grpc" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/metadata" +) + +const grpcServerAddress = "localhost:50051" + +func main() { + // Initialize Sentry + err := sentry.Init(sentry.ClientOptions{ + Dsn: "", + TracesSampleRate: 1.0, + }) + if err != nil { + log.Fatalf("sentry.Init: %s", err) + } + defer sentry.Flush(2 * time.Second) + + // Create a connection to the gRPC server with Sentry interceptors + conn, err := grpc.NewClient( + grpcServerAddress, + grpc.WithTransportCredentials(insecure.NewCredentials()), // Use TLS in production + grpc.WithUnaryInterceptor(sentrygrpc.UnaryClientInterceptor(sentrygrpc.ClientOptions{})), + grpc.WithStreamInterceptor(sentrygrpc.StreamClientInterceptor(sentrygrpc.ClientOptions{})), + ) + if err != nil { + log.Fatalf("Failed to connect to gRPC server: %s", err) + } + defer conn.Close() + + // Create a client for the ExampleService + client := examplepb.NewExampleServiceClient(conn) + + // Perform Unary call + fmt.Println("Performing Unary Call:") + unaryExample(client) + + // Perform Streaming call + fmt.Println("\nPerforming Streaming Call:") + streamExample(client) +} + +func unaryExample(client examplepb.ExampleServiceClient) { + ctx := context.Background() + + // Add metadata to the context + ctx = metadata.NewOutgoingContext(ctx, metadata.Pairs( + "custom-header", "value", + )) + + req := &examplepb.ExampleRequest{ + Message: "Hello, server!", // Change to "error" to simulate an error + } + + res, err := client.UnaryExample(ctx, req) + if err != nil { + fmt.Printf("Unary Call Error: %v\n", err) + sentry.CaptureException(err) + return + } + + fmt.Printf("Unary Response: %s\n", res.Message) +} + +func streamExample(client examplepb.ExampleServiceClient) { + ctx := context.Background() + + // Add metadata to the context + ctx = metadata.NewOutgoingContext(ctx, metadata.Pairs( + "streaming-header", "stream-value", + )) + + stream, err := client.StreamExample(ctx) + if err != nil { + fmt.Printf("Failed to establish stream: %v\n", err) + sentry.CaptureException(err) + return + } + + // Send multiple messages in the stream + messages := []string{"Message 1", "Message 2", "error", "Message 4"} + for _, msg := range messages { + err := stream.Send(&examplepb.ExampleRequest{Message: msg}) + if err != nil { + fmt.Printf("Stream Send Error: %v\n", err) + sentry.CaptureException(err) + return + } + } + + // Close the stream for sending + stream.CloseSend() + + // Receive responses from the server + for { + res, err := stream.Recv() + if err != nil { + fmt.Printf("Stream Recv Error: %v\n", err) + sentry.CaptureException(err) + break + } + fmt.Printf("Stream Response: %s\n", res.Message) + } +} diff --git a/_examples/grpc/server/example.proto b/_examples/grpc/server/example.proto new file mode 100644 index 000000000..356d58f11 --- /dev/null +++ b/_examples/grpc/server/example.proto @@ -0,0 +1,21 @@ +syntax = "proto3"; + +package main; + +option go_package = "github.com/your-username/your-repo/examplepb;examplepb"; + +// ExampleService defines the gRPC service. +service ExampleService { + rpc UnaryExample(ExampleRequest) returns (ExampleResponse); + rpc StreamExample(stream ExampleRequest) returns (stream ExampleResponse); +} + +// ExampleRequest is the request message. +message ExampleRequest { + string message = 1; +} + +// ExampleResponse is the response message. +message ExampleResponse { + string message = 1; +} diff --git a/_examples/grpc/server/examplepb/example.pb.go b/_examples/grpc/server/examplepb/example.pb.go new file mode 100644 index 000000000..84d8b8fbb --- /dev/null +++ b/_examples/grpc/server/examplepb/example.pb.go @@ -0,0 +1,191 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.1 +// protoc v5.29.2 +// source: example.proto + +package examplepb + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// ExampleRequest is the request message. +type ExampleRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExampleRequest) Reset() { + *x = ExampleRequest{} + mi := &file_example_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExampleRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExampleRequest) ProtoMessage() {} + +func (x *ExampleRequest) ProtoReflect() protoreflect.Message { + mi := &file_example_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExampleRequest.ProtoReflect.Descriptor instead. +func (*ExampleRequest) Descriptor() ([]byte, []int) { + return file_example_proto_rawDescGZIP(), []int{0} +} + +func (x *ExampleRequest) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +// ExampleResponse is the response message. +type ExampleResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExampleResponse) Reset() { + *x = ExampleResponse{} + mi := &file_example_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExampleResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExampleResponse) ProtoMessage() {} + +func (x *ExampleResponse) ProtoReflect() protoreflect.Message { + mi := &file_example_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExampleResponse.ProtoReflect.Descriptor instead. +func (*ExampleResponse) Descriptor() ([]byte, []int) { + return file_example_proto_rawDescGZIP(), []int{1} +} + +func (x *ExampleResponse) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +var File_example_proto protoreflect.FileDescriptor + +var file_example_proto_rawDesc = []byte{ + 0x0a, 0x0d, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, + 0x04, 0x6d, 0x61, 0x69, 0x6e, 0x22, 0x2a, 0x0a, 0x0e, 0x45, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, + 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, + 0x65, 0x22, 0x2b, 0x0a, 0x0f, 0x45, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x32, 0x8f, + 0x01, 0x0a, 0x0e, 0x45, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x12, 0x3b, 0x0a, 0x0c, 0x55, 0x6e, 0x61, 0x72, 0x79, 0x45, 0x78, 0x61, 0x6d, 0x70, 0x6c, + 0x65, 0x12, 0x14, 0x2e, 0x6d, 0x61, 0x69, 0x6e, 0x2e, 0x45, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x6d, 0x61, 0x69, 0x6e, 0x2e, 0x45, + 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x40, + 0x0a, 0x0d, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x45, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x12, + 0x14, 0x2e, 0x6d, 0x61, 0x69, 0x6e, 0x2e, 0x45, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x6d, 0x61, 0x69, 0x6e, 0x2e, 0x45, 0x78, 0x61, + 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x28, 0x01, 0x30, 0x01, + 0x42, 0x38, 0x5a, 0x36, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x79, + 0x6f, 0x75, 0x72, 0x2d, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x2f, 0x79, 0x6f, 0x75, + 0x72, 0x2d, 0x72, 0x65, 0x70, 0x6f, 0x2f, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x70, 0x62, + 0x3b, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, +} + +var ( + file_example_proto_rawDescOnce sync.Once + file_example_proto_rawDescData = file_example_proto_rawDesc +) + +func file_example_proto_rawDescGZIP() []byte { + file_example_proto_rawDescOnce.Do(func() { + file_example_proto_rawDescData = protoimpl.X.CompressGZIP(file_example_proto_rawDescData) + }) + return file_example_proto_rawDescData +} + +var file_example_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_example_proto_goTypes = []any{ + (*ExampleRequest)(nil), // 0: main.ExampleRequest + (*ExampleResponse)(nil), // 1: main.ExampleResponse +} +var file_example_proto_depIdxs = []int32{ + 0, // 0: main.ExampleService.UnaryExample:input_type -> main.ExampleRequest + 0, // 1: main.ExampleService.StreamExample:input_type -> main.ExampleRequest + 1, // 2: main.ExampleService.UnaryExample:output_type -> main.ExampleResponse + 1, // 3: main.ExampleService.StreamExample:output_type -> main.ExampleResponse + 2, // [2:4] is the sub-list for method output_type + 0, // [0:2] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_example_proto_init() } +func file_example_proto_init() { + if File_example_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_example_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_example_proto_goTypes, + DependencyIndexes: file_example_proto_depIdxs, + MessageInfos: file_example_proto_msgTypes, + }.Build() + File_example_proto = out.File + file_example_proto_rawDesc = nil + file_example_proto_goTypes = nil + file_example_proto_depIdxs = nil +} diff --git a/_examples/grpc/server/examplepb/example_grpc.pb.go b/_examples/grpc/server/examplepb/example_grpc.pb.go new file mode 100644 index 000000000..56f4b3504 --- /dev/null +++ b/_examples/grpc/server/examplepb/example_grpc.pb.go @@ -0,0 +1,158 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc v5.29.2 +// source: example.proto + +package examplepb + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + ExampleService_UnaryExample_FullMethodName = "/main.ExampleService/UnaryExample" + ExampleService_StreamExample_FullMethodName = "/main.ExampleService/StreamExample" +) + +// ExampleServiceClient is the client API for ExampleService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// ExampleService defines the gRPC service. +type ExampleServiceClient interface { + UnaryExample(ctx context.Context, in *ExampleRequest, opts ...grpc.CallOption) (*ExampleResponse, error) + StreamExample(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[ExampleRequest, ExampleResponse], error) +} + +type exampleServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewExampleServiceClient(cc grpc.ClientConnInterface) ExampleServiceClient { + return &exampleServiceClient{cc} +} + +func (c *exampleServiceClient) UnaryExample(ctx context.Context, in *ExampleRequest, opts ...grpc.CallOption) (*ExampleResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ExampleResponse) + err := c.cc.Invoke(ctx, ExampleService_UnaryExample_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *exampleServiceClient) StreamExample(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[ExampleRequest, ExampleResponse], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &ExampleService_ServiceDesc.Streams[0], ExampleService_StreamExample_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[ExampleRequest, ExampleResponse]{ClientStream: stream} + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type ExampleService_StreamExampleClient = grpc.BidiStreamingClient[ExampleRequest, ExampleResponse] + +// ExampleServiceServer is the server API for ExampleService service. +// All implementations must embed UnimplementedExampleServiceServer +// for forward compatibility. +// +// ExampleService defines the gRPC service. +type ExampleServiceServer interface { + UnaryExample(context.Context, *ExampleRequest) (*ExampleResponse, error) + StreamExample(grpc.BidiStreamingServer[ExampleRequest, ExampleResponse]) error + mustEmbedUnimplementedExampleServiceServer() +} + +// UnimplementedExampleServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedExampleServiceServer struct{} + +func (UnimplementedExampleServiceServer) UnaryExample(context.Context, *ExampleRequest) (*ExampleResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method UnaryExample not implemented") +} +func (UnimplementedExampleServiceServer) StreamExample(grpc.BidiStreamingServer[ExampleRequest, ExampleResponse]) error { + return status.Errorf(codes.Unimplemented, "method StreamExample not implemented") +} +func (UnimplementedExampleServiceServer) mustEmbedUnimplementedExampleServiceServer() {} +func (UnimplementedExampleServiceServer) testEmbeddedByValue() {} + +// UnsafeExampleServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to ExampleServiceServer will +// result in compilation errors. +type UnsafeExampleServiceServer interface { + mustEmbedUnimplementedExampleServiceServer() +} + +func RegisterExampleServiceServer(s grpc.ServiceRegistrar, srv ExampleServiceServer) { + // If the following call pancis, it indicates UnimplementedExampleServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&ExampleService_ServiceDesc, srv) +} + +func _ExampleService_UnaryExample_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ExampleRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ExampleServiceServer).UnaryExample(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ExampleService_UnaryExample_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ExampleServiceServer).UnaryExample(ctx, req.(*ExampleRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ExampleService_StreamExample_Handler(srv interface{}, stream grpc.ServerStream) error { + return srv.(ExampleServiceServer).StreamExample(&grpc.GenericServerStream[ExampleRequest, ExampleResponse]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type ExampleService_StreamExampleServer = grpc.BidiStreamingServer[ExampleRequest, ExampleResponse] + +// ExampleService_ServiceDesc is the grpc.ServiceDesc for ExampleService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var ExampleService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "main.ExampleService", + HandlerType: (*ExampleServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "UnaryExample", + Handler: _ExampleService_UnaryExample_Handler, + }, + }, + Streams: []grpc.StreamDesc{ + { + StreamName: "StreamExample", + Handler: _ExampleService_StreamExample_Handler, + ServerStreams: true, + ClientStreams: true, + }, + }, + Metadata: "example.proto", +} diff --git a/_examples/grpc/server/main.go b/_examples/grpc/server/main.go new file mode 100644 index 000000000..defe1b09a --- /dev/null +++ b/_examples/grpc/server/main.go @@ -0,0 +1,95 @@ +package main + +import ( + "context" + "fmt" + "grpcdemo/cmd/server/examplepb" + "log" + "net" + "time" + + "github.com/getsentry/sentry-go" + sentrygrpc "github.com/getsentry/sentry-go/grpc" + "google.golang.org/grpc" + "google.golang.org/grpc/metadata" +) + +const grpcPort = ":50051" + +// ExampleServiceServer is the server implementation for the ExampleService. +type ExampleServiceServer struct { + examplepb.UnimplementedExampleServiceServer +} + +// UnaryExample handles unary gRPC requests. +func (s *ExampleServiceServer) UnaryExample(ctx context.Context, req *examplepb.ExampleRequest) (*examplepb.ExampleResponse, error) { + md, _ := metadata.FromIncomingContext(ctx) + fmt.Printf("Received Unary Request: %v\nMetadata: %v\n", req.Message, md) + + // Simulate an error for demonstration + if req.Message == "error" { + return nil, fmt.Errorf("simulated unary error") + } + + return &examplepb.ExampleResponse{Message: fmt.Sprintf("Hello, %s!", req.Message)}, nil +} + +// StreamExample handles bidirectional streaming gRPC requests. +func (s *ExampleServiceServer) StreamExample(stream examplepb.ExampleService_StreamExampleServer) error { + for { + req, err := stream.Recv() + if err != nil { + fmt.Printf("Stream Recv Error: %v\n", err) + return err + } + + fmt.Printf("Received Stream Message: %v\n", req.Message) + + if req.Message == "error" { + return fmt.Errorf("simulated stream error") + } + + err = stream.Send(&examplepb.ExampleResponse{Message: fmt.Sprintf("Echo: %s", req.Message)}) + if err != nil { + fmt.Printf("Stream Send Error: %v\n", err) + return err + } + } +} + +func main() { + // Initialize Sentry + err := sentry.Init(sentry.ClientOptions{ + Dsn: "", + TracesSampleRate: 1.0, + }) + if err != nil { + log.Fatalf("sentry.Init: %s", err) + } + defer sentry.Flush(2 * time.Second) + + // Create a new gRPC server with Sentry interceptors + server := grpc.NewServer( + grpc.UnaryInterceptor(sentrygrpc.UnaryServerInterceptor(sentrygrpc.ServerOptions{ + Repanic: true, + CaptureRequestBody: true, + })), + grpc.StreamInterceptor(sentrygrpc.StreamServerInterceptor(sentrygrpc.ServerOptions{ + Repanic: true, + })), + ) + + // Register the ExampleService + examplepb.RegisterExampleServiceServer(server, &ExampleServiceServer{}) + + // Start the server + listener, err := net.Listen("tcp", grpcPort) + if err != nil { + log.Fatalf("Failed to listen on port %s: %v", grpcPort, err) + } + + fmt.Printf("gRPC server is running on %s\n", grpcPort) + if err := server.Serve(listener); err != nil { + log.Fatalf("Failed to serve: %v", err) + } +} From 77a6c95180412cd1aa65a6f8bb94901a0b7fda65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emir=20Ribi=C4=87?= Date: Fri, 27 Dec 2024 10:03:41 +0100 Subject: [PATCH 6/9] fix tests --- grpc/client_test.go | 3 --- grpc/server_test.go | 12 ++++-------- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/grpc/client_test.go b/grpc/client_test.go index 404145ede..8e51414ee 100644 --- a/grpc/client_test.go +++ b/grpc/client_test.go @@ -16,8 +16,6 @@ import ( const defaultClientOperationName = "grpc.client" func TestClientOptions_SetDefaults(t *testing.T) { - t.Parallel() - tests := map[string]struct { options sentrygrpc.ClientOptions assertions func(t *testing.T, options sentrygrpc.ClientOptions) @@ -53,7 +51,6 @@ func TestClientOptions_SetDefaults(t *testing.T) { for name, test := range tests { t.Run(name, func(t *testing.T) { - t.Parallel() test.options.SetDefaults() test.assertions(t, test.options) diff --git a/grpc/server_test.go b/grpc/server_test.go index 933b865e2..a11d46652 100644 --- a/grpc/server_test.go +++ b/grpc/server_test.go @@ -19,7 +19,6 @@ import ( const defaultServerOperationName = "grpc.server" func TestServerOptions_SetDefaults(t *testing.T) { - t.Parallel() tests := map[string]struct { options sentrygrpc.ServerOptions assertions func(t *testing.T, options sentrygrpc.ServerOptions) @@ -74,8 +73,6 @@ func TestServerOptions_SetDefaults(t *testing.T) { for name, test := range tests { t.Run(name, func(t *testing.T) { - t.Parallel() - test.options.SetDefaults() test.assertions(t, test.options) @@ -236,7 +233,6 @@ func (w *wrappedServerStream) Context() context.Context { } func TestStreamServerInterceptor(t *testing.T) { - t.Parallel() tests := map[string]struct { options sentrygrpc.ServerOptions handler grpc.StreamHandler @@ -275,7 +271,7 @@ func TestStreamServerInterceptor(t *testing.T) { }, expectedErr: "", expectedMetadata: false, - expectedEvent: false, // The panic is re-raised, event capture may depend on hub state + expectedEvent: true, }, "Metadata is propagated": { options: sentrygrpc.ServerOptions{}, @@ -294,7 +290,6 @@ func TestStreamServerInterceptor(t *testing.T) { for name, test := range tests { t.Run(name, func(t *testing.T) { - t.Parallel() eventsCh := make(chan *sentry.Event, 1) transactionsCh := make(chan *sentry.Event, 1) @@ -312,13 +307,12 @@ func TestStreamServerInterceptor(t *testing.T) { if err != nil { t.Fatal(err) } - defer sentry.Flush(2 * time.Second) interceptor := sentrygrpc.StreamServerInterceptor(test.options) // Simulate a server stream stream := &wrappedServerStream{ - ServerStream: nil, // Mock implementation or use a testing framework + ServerStream: nil, ctx: metadata.NewIncomingContext(context.Background(), metadata.Pairs("key", "value")), } @@ -357,6 +351,8 @@ func TestStreamServerInterceptor(t *testing.T) { assert.NotNil(t, recovered, "Expected panic to be re-raised") assert.Equal(t, "test panic", recovered, "Panic value should match") } + + sentry.Flush(2 * time.Second) }) } } From 16ee674d0af18779e25b688470be8e55a91838c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emir=20Ribi=C4=87?= Date: Fri, 27 Dec 2024 10:07:00 +0100 Subject: [PATCH 7/9] add span origin --- grpc/server.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/grpc/server.go b/grpc/server.go index e8d9dc0d4..016db7316 100644 --- a/grpc/server.go +++ b/grpc/server.go @@ -138,6 +138,7 @@ func UnaryServerInterceptor(opts ServerOptions) grpc.UnaryServerInterceptor { sentry.WithOpName(opts.OperationName), sentry.WithDescription(info.FullMethod), sentry.WithTransactionSource(sentry.SourceURL), + sentry.WithSpanOrigin(sentry.SpanOriginGrpc), } transaction := sentry.StartTransaction( @@ -204,6 +205,7 @@ func StreamServerInterceptor(opts ServerOptions) grpc.StreamServerInterceptor { sentry.WithOpName(opts.OperationName), sentry.WithDescription(info.FullMethod), sentry.WithTransactionSource(sentry.SourceURL), + sentry.WithSpanOrigin(sentry.SpanOriginGrpc), } transaction := sentry.StartTransaction( From 2f47b4373320fc0965db0126fc55c359c6ac3750 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emir=20Ribi=C4=87?= Date: Fri, 27 Dec 2024 10:23:04 +0100 Subject: [PATCH 8/9] downgrade grpc dependency version --- grpc/go.mod | 6 +++--- grpc/go.sum | 33 ++++----------------------------- 2 files changed, 7 insertions(+), 32 deletions(-) diff --git a/grpc/go.mod b/grpc/go.mod index 00114b6c2..194669eb1 100644 --- a/grpc/go.mod +++ b/grpc/go.mod @@ -1,14 +1,14 @@ module github.com/getsentry/sentry-go/grpc -go 1.22 +go 1.21 replace github.com/getsentry/sentry-go => ../ require ( github.com/getsentry/sentry-go v0.30.0 - github.com/stretchr/testify v1.8.2 + github.com/stretchr/testify v1.10.0 google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 - google.golang.org/grpc v1.69.2 + google.golang.org/grpc v1.67.3 ) require ( diff --git a/grpc/go.sum b/grpc/go.sum index e43760af9..ebaece8c9 100644 --- a/grpc/go.sum +++ b/grpc/go.sum @@ -1,19 +1,10 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= -github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= -github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -26,23 +17,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY= -go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE= -go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE= -go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY= -go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk= -go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0= -go.opentelemetry.io/otel/sdk/metric v1.31.0 h1:i9hxxLJF/9kkvfHppyLL55aW7iIJz4JjxTeYusH7zMc= -go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8= -go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys= -go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= @@ -53,13 +29,12 @@ golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 h1:TqExAhdPaB60Ux47Cn0oLV07rGnxZzIsaRhQaqS666A= google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA= -google.golang.org/grpc v1.69.2 h1:U3S9QEtbXC0bYNvRtcoklF3xGtLViumSYxWykJS+7AU= -google.golang.org/grpc v1.69.2/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= +google.golang.org/grpc v1.67.3 h1:OgPcDAFKHnH8X3O4WcO4XUc8GRDeKsKReqbQtiCj7N8= +google.golang.org/grpc v1.67.3/go.mod h1:YGaHCc6Oap+FzBJTZLBzkGSYt/cvGPFTPxkn7QfSU8s= google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 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/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 8c75a66a32db66eac6354045a486d66db1cc70c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emir=20Ribi=C4=87?= Date: Fri, 27 Dec 2024 22:40:29 +0100 Subject: [PATCH 9/9] update changelog --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25f6b7237..b9d754c01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,9 @@ ### Features -Add ability to override `hub` in `context` for integrations that use custom context ([#931](https://github.com/getsentry/sentry-go/pull/931)) +- Add ability to override `hub` in `context` for integrations that use custom context ([#931](https://github.com/getsentry/sentry-go/pull/931)) + +- Add `grpc` integration ([#938](https://github.com/getsentry/sentry-go/pull/938)) ## 0.30.0