diff --git a/v3/integrations/nrgrpc/nrgrpc_client_test.go b/v3/integrations/nrgrpc/nrgrpc_client_test.go index 902e28a1a..f91d568d4 100644 --- a/v3/integrations/nrgrpc/nrgrpc_client_test.go +++ b/v3/integrations/nrgrpc/nrgrpc_client_test.go @@ -95,15 +95,15 @@ func TestUnaryClientInterceptor(t *testing.T) { client := testapp.NewTestApplicationClient(conn) resp, err := client.DoUnaryUnary(ctx, &testapp.Message{}) - if nil != err { + if err != nil { t.Fatal("client call to DoUnaryUnary failed", err) } var hdrs map[string][]string err = json.Unmarshal([]byte(resp.Text), &hdrs) - if nil != err { + if err != nil { t.Fatal("cannot unmarshall client response", err) } - if hdr, ok := hdrs["newrelic"]; !ok || len(hdr) != 1 || "" == hdr[0] { + if hdr, ok := hdrs["newrelic"]; !ok || len(hdr) != 1 || hdr[0] == "" { t.Error("distributed trace header not sent", hdrs) } txn.End() @@ -175,7 +175,7 @@ func TestUnaryStreamClientInterceptor(t *testing.T) { client := testapp.NewTestApplicationClient(conn) stream, err := client.DoUnaryStream(ctx, &testapp.Message{}) - if nil != err { + if err != nil { t.Fatal("client call to DoUnaryStream failed", err) } var recved int @@ -184,15 +184,15 @@ func TestUnaryStreamClientInterceptor(t *testing.T) { if err == io.EOF { break } - if nil != err { + if err != nil { t.Fatal("error receiving message", err) } var hdrs map[string][]string err = json.Unmarshal([]byte(msg.Text), &hdrs) - if nil != err { + if err != nil { t.Fatal("cannot unmarshall client response", err) } - if hdr, ok := hdrs["newrelic"]; !ok || len(hdr) != 1 || "" == hdr[0] { + if hdr, ok := hdrs["newrelic"]; !ok || len(hdr) != 1 || hdr[0] == "" { t.Error("distributed trace header not sent", hdrs) } recved++ @@ -269,11 +269,11 @@ func TestStreamUnaryClientInterceptor(t *testing.T) { client := testapp.NewTestApplicationClient(conn) stream, err := client.DoStreamUnary(ctx) - if nil != err { + if err != nil { t.Fatal("client call to DoStreamUnary failed", err) } for i := 0; i < 3; i++ { - if err := stream.Send(&testapp.Message{Text: "Hello DoStreamUnary"}); nil != err { + if err := stream.Send(&testapp.Message{Text: "Hello DoStreamUnary"}); err != nil { if err == io.EOF { break } @@ -281,15 +281,15 @@ func TestStreamUnaryClientInterceptor(t *testing.T) { } } msg, err := stream.CloseAndRecv() - if nil != err { + if err != nil { t.Fatal("failure to CloseAndRecv", err) } var hdrs map[string][]string err = json.Unmarshal([]byte(msg.Text), &hdrs) - if nil != err { + if err != nil { t.Fatal("cannot unmarshall client response", err) } - if hdr, ok := hdrs["newrelic"]; !ok || len(hdr) != 1 || "" == hdr[0] { + if hdr, ok := hdrs["newrelic"]; !ok || len(hdr) != 1 || hdr[0] == "" { t.Error("distributed trace header not sent", hdrs) } txn.End() @@ -361,7 +361,7 @@ func TestStreamStreamClientInterceptor(t *testing.T) { client := testapp.NewTestApplicationClient(conn) stream, err := client.DoStreamStream(ctx) - if nil != err { + if err != nil { t.Fatal("client call to DoStreamStream failed", err) } waitc := make(chan struct{}) @@ -378,10 +378,10 @@ func TestStreamStreamClientInterceptor(t *testing.T) { } var hdrs map[string][]string err = json.Unmarshal([]byte(msg.Text), &hdrs) - if nil != err { + if err != nil { t.Fatal("cannot unmarshall client response", err) } - if hdr, ok := hdrs["newrelic"]; !ok || len(hdr) != 1 || "" == hdr[0] { + if hdr, ok := hdrs["newrelic"]; !ok || len(hdr) != 1 || hdr[0] == "" { t.Error("distributed trace header not sent", hdrs) } recved++ @@ -473,18 +473,18 @@ func TestClientUnaryMetadata(t *testing.T) { client := testapp.NewTestApplicationClient(conn) resp, err := client.DoUnaryUnary(ctx, &testapp.Message{}) - if nil != err { + if err != nil { t.Fatal("client call to DoUnaryUnary failed", err) } var hdrs map[string][]string err = json.Unmarshal([]byte(resp.Text), &hdrs) - if nil != err { + if err != nil { t.Fatal("cannot unmarshall client response", err) } - if hdr, ok := hdrs["newrelic"]; !ok || len(hdr) != 1 || "" == hdr[0] || "payload" == hdr[0] { + if hdr, ok := hdrs["newrelic"]; !ok || len(hdr) != 1 || hdr[0] == "" || hdr[0] == "payload" { t.Error("distributed trace header not sent", hdrs) } - if hdr, ok := hdrs["testing"]; !ok || len(hdr) != 1 || "hello world" != hdr[0] { + if hdr, ok := hdrs["testing"]; !ok || len(hdr) != 1 || hdr[0] != "hello world" { t.Error("testing header not sent", hdrs) } } @@ -496,12 +496,12 @@ func TestNilTxnClientUnary(t *testing.T) { client := testapp.NewTestApplicationClient(conn) resp, err := client.DoUnaryUnary(context.Background(), &testapp.Message{}) - if nil != err { + if err != nil { t.Fatal("client call to DoUnaryUnary failed", err) } var hdrs map[string][]string err = json.Unmarshal([]byte(resp.Text), &hdrs) - if nil != err { + if err != nil { t.Fatal("cannot unmarshall client response", err) } if _, ok := hdrs["newrelic"]; ok { @@ -516,11 +516,11 @@ func TestNilTxnClientStreaming(t *testing.T) { client := testapp.NewTestApplicationClient(conn) stream, err := client.DoStreamUnary(context.Background()) - if nil != err { + if err != nil { t.Fatal("client call to DoStreamUnary failed", err) } for i := 0; i < 3; i++ { - if err := stream.Send(&testapp.Message{Text: "Hello DoStreamUnary"}); nil != err { + if err := stream.Send(&testapp.Message{Text: "Hello DoStreamUnary"}); err != nil { if err == io.EOF { break } @@ -528,12 +528,12 @@ func TestNilTxnClientStreaming(t *testing.T) { } } msg, err := stream.CloseAndRecv() - if nil != err { + if err != nil { t.Fatal("failure to CloseAndRecv", err) } var hdrs map[string][]string err = json.Unmarshal([]byte(msg.Text), &hdrs) - if nil != err { + if err != nil { t.Fatal("cannot unmarshall client response", err) } if _, ok := hdrs["newrelic"]; ok { @@ -557,7 +557,7 @@ func TestClientStreamingError(t *testing.T) { defer cancel() ctx = newrelic.NewContext(ctx, txn) _, err := client.DoUnaryStream(ctx, &testapp.Message{}) - if nil == err { + if err == nil { t.Fatal("client call to DoUnaryStream did not return error") } txn.End() diff --git a/v3/integrations/nrgrpc/nrgrpc_doc.go b/v3/integrations/nrgrpc/nrgrpc_doc.go index 075b4f610..c059d13fe 100644 --- a/v3/integrations/nrgrpc/nrgrpc_doc.go +++ b/v3/integrations/nrgrpc/nrgrpc_doc.go @@ -1,6 +1,7 @@ // Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +// // Package nrgrpc instruments https://github.com/grpc/grpc-go. // // This package can be used to instrument gRPC servers and gRPC clients. @@ -9,18 +10,58 @@ // // To instrument a gRPC server, use UnaryServerInterceptor and // StreamServerInterceptor with your newrelic.Application to create server -// interceptors to pass to grpc.NewServer. Example: +// interceptors to pass to grpc.NewServer. // +// The results of these calls are reported as errors or as informational +// messages (of levels OK, Info, Warning, or Error) based on the gRPC status +// code they return. // -// app, _ := newrelic.NewApplication( -// newrelic.ConfigAppName("gRPC Server"), -// newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), -// newrelic.ConfigDebugLogger(os.Stdout), -// ) -// server := grpc.NewServer( -// grpc.UnaryInterceptor(nrgrpc.UnaryServerInterceptor(app)), -// grpc.StreamInterceptor(nrgrpc.StreamServerInterceptor(app)), -// ) +// In the simplest case, simply add interceptors as in the following example: +// +// app, _ := newrelic.NewApplication( +// newrelic.ConfigAppName("gRPC Server"), +// newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), +// newrelic.ConfigDebugLogger(os.Stdout), +// ) +// server := grpc.NewServer( +// grpc.UnaryInterceptor(nrgrpc.UnaryServerInterceptor(app)), +// grpc.StreamInterceptor(nrgrpc.StreamServerInterceptor(app)), +// ) +// +// The disposition of each, in terms of how to report each of the various +// gRPC status codes, is determined by a built-in set of defaults: +// OK OK +// Info AlreadyExists, Canceled, InvalidArgument, NotFound, +// Unauthenticated +// Warning Aborted, DeadlineExceeded, FailedPrecondition, OutOfRange, +// PermissionDenied, ResourceExhausted, Unavailable +// Error DataLoss, Internal, Unknown, Unimplemented +// +// These +// may be overridden on a case-by-case basis using `WithStatusHandler()` +// options to each `UnaryServerInterceptor()` or `StreamServerInterceptor()` +// call, or globally via the `Configure()` function. +// +// For example, to report DeadlineExceeded as an error and NotFound +// as a warning, for the UnaryInterceptor only: +// server := grpc.NewServer( +// grpc.UnaryInterceptor(nrgrpc.UnaryServerInterceptor(app, +// nrgrpc.WithStatusHandler(codes.DeadlineExceeded, nrgrpc.ErrorInterceptorStatusHandler), +// nrgrpc.WithStatusHandler(codes.NotFound, nrgrpc.WarningInterceptorStatusHandler)), +// grpc.StreamInterceptor(nrgrpc.StreamServerInterceptor(app)), +// ) +// +// If you wanted to make those two changes to the overall default behavior, so they +// apply to all subsequently declared interceptors: +// nrgrpc.Configure( +// nrgrpc.WithStatusHandler(codes.DeadlineExceeded, nrgrpc.ErrorInterceptorStatusHandler), +// nrgrpc.WithStatusHandler(codes.NotFound, nrgrpc.WarningInterceptorStatusHandler), +// ) +// server := grpc.NewServer( +// grpc.UnaryInterceptor(nrgrpc.UnaryServerInterceptor(app)), +// grpc.StreamInterceptor(nrgrpc.StreamServerInterceptor(app)), +// ) +// In this case the new behavior for those two status codes applies to both interceptors. // // These interceptors create transactions for inbound calls. The transaction is // added to the call context and can be accessed in your method handlers diff --git a/v3/integrations/nrgrpc/nrgrpc_server.go b/v3/integrations/nrgrpc/nrgrpc_server.go index e8119c66e..afca8033b 100644 --- a/v3/integrations/nrgrpc/nrgrpc_server.go +++ b/v3/integrations/nrgrpc/nrgrpc_server.go @@ -1,6 +1,36 @@ // Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +// +// This integration instruments grpc service calls via +// UnaryServerInterceptor() and StreamServerInterceptor() functions. +// +// The results of these calls are reported as errors or as informational +// messages (of levels OK, Info, Warning, or Error) based on the gRPC status +// code they return. +// +// In the simplest case, simply add interceptors as in the following example: +// +// app, _ := newrelic.NewApplication( +// newrelic.ConfigAppName("gRPC Server"), +// newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), +// newrelic.ConfigDebugLogger(os.Stdout), +// ) +// server := grpc.NewServer( +// grpc.UnaryInterceptor(nrgrpc.UnaryServerInterceptor(app)), +// grpc.StreamInterceptor(nrgrpc.StreamServerInterceptor(app)), +// ) +// +// The disposition of each, in terms of how to report each of the various +// gRPC status codes, is determined by a built-in set of defaults. These +// may be overridden on a case-by-case basis using WithStatusHandler() +// options to each UnaryServerInterceptor() or StreamServerInterceptor() +// call, or globally via the Configure() function. +// +// Full example: +// https://github.com/newrelic/go-agent/blob/master/v3/integrations/nrgrpc/example/server/server.go +// + package nrgrpc import ( @@ -10,6 +40,7 @@ import ( "github.com/newrelic/go-agent/v3/newrelic" "google.golang.org/grpc" + "google.golang.org/grpc/codes" "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" ) @@ -42,6 +73,173 @@ func startTransaction(ctx context.Context, app *newrelic.Application, fullMethod return txn } +// +// ErrorHandler is the type of a gRPC status handler function. +// Normally the supplied set of ErrorHandler functions will suffice, but +// a custom handler may be crafted by the user and installed as a handler +// if needed. +// +type ErrorHandler func(context.Context, *newrelic.Transaction, *status.Status) + +// +// Internal registry of handlers associated with various +// status codes. +// +type statusHandlerMap map[codes.Code]ErrorHandler + +// +// interceptorStatusHandlerRegistry is the current default set of handlers +// used by each interceptor. +// +var interceptorStatusHandlerRegistry = statusHandlerMap{ + codes.OK: OKInterceptorStatusHandler, + codes.Canceled: InfoInterceptorStatusHandler, + codes.Unknown: ErrorInterceptorStatusHandler, + codes.InvalidArgument: InfoInterceptorStatusHandler, + codes.DeadlineExceeded: WarningInterceptorStatusHandler, + codes.NotFound: InfoInterceptorStatusHandler, + codes.AlreadyExists: InfoInterceptorStatusHandler, + codes.PermissionDenied: WarningInterceptorStatusHandler, + codes.ResourceExhausted: WarningInterceptorStatusHandler, + codes.FailedPrecondition: WarningInterceptorStatusHandler, + codes.Aborted: WarningInterceptorStatusHandler, + codes.OutOfRange: WarningInterceptorStatusHandler, + codes.Unimplemented: ErrorInterceptorStatusHandler, + codes.Internal: ErrorInterceptorStatusHandler, + codes.Unavailable: WarningInterceptorStatusHandler, + codes.DataLoss: ErrorInterceptorStatusHandler, + codes.Unauthenticated: InfoInterceptorStatusHandler, +} + +type handlerOption func(statusHandlerMap) + +// +// WithStatusHandler() indicates a handler function to be used to +// report the indicated gRPC status. Zero or more of these may be +// given to the Configure(), StreamServiceInterceptor(), or +// UnaryServiceInterceptor() functions. +// +// The ErrorHandler parameter is generally one of the provided standard +// reporting functions: +// OKInterceptorStatusHandler // report the operation as successful +// ErrorInterceptorStatusHandler // report the operation as an error +// WarningInterceptorStatusHandler // report the operation as a warning +// InfoInterceptorStatusHandler // report the operation as an informational message +// +// The following reporting function should only be used if you know for sure +// you want this. It does not report the error in any way at all, but completely +// ignores it. +// IgnoreInterceptorStatusHandler // report the operation as successful +// +// Finally, if you have a custom reporting need that isn't covered by the standard +// handler functions, you can create your own handler function as +// func myHandler(ctx context.Context, txn *newrelic.Transaction, s *status.Status) { +// ... +// } +// Within the function, do whatever you need to do with the txn parameter to report the +// gRPC status passed as s. If needed, the context is also passed to your function. +// +// If you wish to use your custom handler for a code such as codes.NotFound, you would +// include the parameter +// WithStatusHandler(codes.NotFound, myHandler) +// to your Configure(), StreamServiceInterceptor(), or UnaryServiceInterceptor() function. +// +func WithStatusHandler(c codes.Code, h ErrorHandler) handlerOption { + return func(m statusHandlerMap) { + m[c] = h + } +} + +// +// Configure() takes a list of WithStatusHandler() options and sets them +// as the new default handlers for the specified gRPC status codes, in the same +// way as if WithStatusHandler() were given to the StreamServiceInterceptor() +// or UnaryServiceInterceptor() functions (q.v.); however, in this case the new handlers +// become the default for any subsequent interceptors created by the above functions. +// +func Configure(options ...handlerOption) { + for _, option := range options { + option(interceptorStatusHandlerRegistry) + } +} + +// +// Standard handler: IGNORE +// This does not report anything at all in the context. +// +func IgnoreInterceptorStatusHandler(_ context.Context, _ *newrelic.Transaction, _ *status.Status) {} + +// +// Standard handler: OK +// Reports only that the RPC call was successful by setting the web response header +// value. +// +func OKInterceptorStatusHandler(ctx context.Context, txn *newrelic.Transaction, s *status.Status) { + txn.SetWebResponse(nil).WriteHeader(int(codes.OK)) +} + +// +// Standard handler: ERROR +// Reports the transaction as an error, with the relevant error messages and +// contextual information gleaned from the error value received from the RPC call. +// +func ErrorInterceptorStatusHandler(ctx context.Context, txn *newrelic.Transaction, s *status.Status) { + txn.SetWebResponse(nil).WriteHeader(int(codes.OK)) + txn.NoticeError(&newrelic.Error{ + Message: s.Message(), + Class: "gRPC Status: " + s.Code().String(), + }) + txn.AddAttribute("GrpcStatusLevel", "error") + txn.AddAttribute("GrpcStatusMessage", s.Message()) + txn.AddAttribute("GrpcStatusCode", s.Code().String()) +} + +// examples +// Class "LoginError", Attr: "username": username + +// +// Standard handler: WARNING +// Reports the transaction's status with attributes containing information gleaned +// from the error value returned, but does not count this as an error. +// +func WarningInterceptorStatusHandler(ctx context.Context, txn *newrelic.Transaction, s *status.Status) { + txn.SetWebResponse(nil).WriteHeader(int(codes.OK)) + txn.AddAttribute("GrpcStatusLevel", "warning") + txn.AddAttribute("GrpcStatusMessage", s.Message()) + txn.AddAttribute("GrpcStatusCode", s.Code().String()) +} + +// +// Standard handler: INFO +// Reports the transaction's status with attributes containing information gleaned +// from the error value returned, but does not count this as an error. +// +func InfoInterceptorStatusHandler(ctx context.Context, txn *newrelic.Transaction, s *status.Status) { + txn.SetWebResponse(nil).WriteHeader(int(codes.OK)) + txn.AddAttribute("GrpcStatusLevel", "info") + txn.AddAttribute("GrpcStatusMessage", s.Message()) + txn.AddAttribute("GrpcStatusCode", s.Code().String()) +} + +// +// Standard handler: DEFAULT +// The DefaultInterceptorStatusHander is used for any status code which is not +// explicitly assigned a handler. +// +var DefaultInterceptorStatusHandler = InfoInterceptorStatusHandler + +// +// Common routine for reporting any kind of interceptor. +// +func reportInterceptorStatus(ctx context.Context, txn *newrelic.Transaction, handlers statusHandlerMap, err error) { + grpcStatus := status.Convert(err) + handler, ok := handlers[grpcStatus.Code()] + if !ok { + handler = DefaultInterceptorStatusHandler + } + handler(ctx, txn, grpcStatus) +} + // UnaryServerInterceptor instruments server unary RPCs. // // Use this function with grpc.UnaryInterceptor and a newrelic.Application to @@ -65,26 +263,46 @@ func startTransaction(ctx context.Context, app *newrelic.Application, fullMethod // These interceptors add the transaction to the call context so it may be // accessed in your method handlers using newrelic.FromContext. // -// Full example: -// https://github.com/newrelic/go-agent/blob/master/v3/integrations/nrgrpc/example/server/server.go +// The nrgrpc integration has a built-in set of handlers for each gRPC status +// code encountered. Serious errors are reported as error traces à la the +// newrelic.NoticeError() function, while the others are reported but not +// counted as errors. +// +// If you wish to change this behavior, you may do so at a global level for +// all intercepted functions by calling the Configure() function, passing +// any number of WithStatusHandler(code, handler) functions as parameters. +// +// You can specify a custom set of handlers with each interceptor creation by adding +// WithStatusHandler() calls at the end of the StreamInterceptor() call's parameter list, +// like so: +// grpc.UnaryInterceptor(nrgrpc.UnaryServerInterceptor(app, +// nrgrpc.WithStatusHandler(codes.OutOfRange, nrgrpc.WarningInterceptorStatusHandler), +// nrgrpc.WithStatusHandler(codes.Unimplemented, nrgrpc.InfoInterceptorStatusHandler))) +// In this case, those two handlers are used (along with the current defaults for the other status +// codes) only for that interceptor. // -func UnaryServerInterceptor(app *newrelic.Application) grpc.UnaryServerInterceptor { +func UnaryServerInterceptor(app *newrelic.Application, options ...handlerOption) grpc.UnaryServerInterceptor { if app == nil { return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { return handler(ctx, req) } } + localHandlerMap := make(statusHandlerMap) + for code, handler := range interceptorStatusHandlerRegistry { + localHandlerMap[code] = handler + } + for _, option := range options { + option(localHandlerMap) + } + return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) { txn := startTransaction(ctx, app, info.FullMethod) defer txn.End() ctx = newrelic.NewContext(ctx, txn) resp, err = handler(ctx, req) - txn.SetWebResponse(nil).WriteHeader(int(status.Code(err))) - if err != nil { - txn.NoticeError(err) - } + reportInterceptorStatus(ctx, txn, localHandlerMap, err) return } } @@ -114,40 +332,29 @@ func newWrappedServerStream(stream grpc.ServerStream, txn *newrelic.Transaction) // UnaryServerInterceptor and StreamServerInterceptor to instrument unary and // streaming calls. // -// Example: +// See the notes and examples for the UnaryServerInterceptor() function. // -// app, _ := newrelic.NewApplication( -// newrelic.ConfigAppName("gRPC Server"), -// newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), -// newrelic.ConfigDebugLogger(os.Stdout), -// ) -// server := grpc.NewServer( -// grpc.UnaryInterceptor(nrgrpc.UnaryServerInterceptor(app)), -// grpc.StreamInterceptor(nrgrpc.StreamServerInterceptor(app)), -// ) -// -// These interceptors add the transaction to the call context so it may be -// accessed in your method handlers using newrelic.FromContext. -// -// Full example: -// https://github.com/newrelic/go-agent/blob/master/v3/integrations/nrgrpc/example/server/server.go -// -func StreamServerInterceptor(app *newrelic.Application) grpc.StreamServerInterceptor { +func StreamServerInterceptor(app *newrelic.Application, options ...handlerOption) grpc.StreamServerInterceptor { if app == nil { return func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { return handler(srv, ss) } } + localHandlerMap := make(statusHandlerMap) + for code, handler := range interceptorStatusHandlerRegistry { + localHandlerMap[code] = handler + } + for _, option := range options { + option(localHandlerMap) + } + return func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { txn := startTransaction(ss.Context(), app, info.FullMethod) defer txn.End() err := handler(srv, newWrappedServerStream(ss, txn)) - txn.SetWebResponse(nil).WriteHeader(int(status.Code(err))) - if err != nil { - txn.NoticeError(err) - } + reportInterceptorStatus(ss.Context(), txn, localHandlerMap, err) return err } } diff --git a/v3/integrations/nrgrpc/nrgrpc_server_test.go b/v3/integrations/nrgrpc/nrgrpc_server_test.go index 4dce85bdf..d77004c0d 100644 --- a/v3/integrations/nrgrpc/nrgrpc_server_test.go +++ b/v3/integrations/nrgrpc/nrgrpc_server_test.go @@ -62,7 +62,7 @@ func TestUnaryServerInterceptor(t *testing.T) { txn := app.StartTransaction("client") ctx := newrelic.NewContext(context.Background(), txn) _, err := client.DoUnaryUnary(ctx, &testapp.Message{}) - if nil != err { + if err != nil { t.Fatal("unable to call client DoUnaryUnary", err) } @@ -152,7 +152,7 @@ func TestUnaryServerInterceptorError(t *testing.T) { client := testapp.NewTestApplicationClient(conn) _, err := client.DoUnaryUnaryError(context.Background(), &testapp.Message{}) - if nil == err { + if err == nil { t.Fatal("DoUnaryUnaryError should have returned an error") } @@ -181,10 +181,14 @@ func TestUnaryServerInterceptorError(t *testing.T) { "sampled": internal.MatchAnything, "traceId": internal.MatchAnything, }, - UserAttributes: map[string]interface{}{}, + UserAttributes: map[string]interface{}{ + "GrpcStatusMessage": "oooooops!", + "GrpcStatusCode": "DataLoss", + "GrpcStatusLevel": "error", + }, AgentAttributes: map[string]interface{}{ - "httpResponseCode": 15, - "http.statusCode": 15, + "httpResponseCode": 0, + "http.statusCode": 0, "request.headers.contentType": "application/grpc", "request.method": "TestApplication/DoUnaryUnaryError", "request.uri": "grpc://bufnet/TestApplication/DoUnaryUnaryError", @@ -192,8 +196,8 @@ func TestUnaryServerInterceptorError(t *testing.T) { }}) app.ExpectErrorEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ - "error.class": "15", - "error.message": "response code 15", + "error.class": "gRPC Error: DataLoss", + "error.message": "oooooops!", "guid": internal.MatchAnything, "priority": internal.MatchAnything, "sampled": internal.MatchAnything, @@ -202,36 +206,19 @@ func TestUnaryServerInterceptorError(t *testing.T) { "transactionName": "WebTransaction/Go/TestApplication/DoUnaryUnaryError", }, AgentAttributes: map[string]interface{}{ - "httpResponseCode": 15, - "http.statusCode": 15, + "httpResponseCode": 0, + "http.statusCode": 0, "request.headers.User-Agent": internal.MatchAnything, "request.headers.userAgent": internal.MatchAnything, "request.headers.contentType": "application/grpc", "request.method": "TestApplication/DoUnaryUnaryError", "request.uri": "grpc://bufnet/TestApplication/DoUnaryUnaryError", }, - UserAttributes: map[string]interface{}{}, - }, { - Intrinsics: map[string]interface{}{ - "error.class": internal.MatchAnything, - "error.message": "rpc error: code = DataLoss desc = oooooops!", - "guid": internal.MatchAnything, - "priority": internal.MatchAnything, - "sampled": internal.MatchAnything, - "spanId": internal.MatchAnything, - "traceId": internal.MatchAnything, - "transactionName": "WebTransaction/Go/TestApplication/DoUnaryUnaryError", + UserAttributes: map[string]interface{}{ + "GrpcStatusMessage": "oooooops!", + "GrpcStatusCode": "DataLoss", + "GrpcStatusLevel": "error", }, - AgentAttributes: map[string]interface{}{ - "httpResponseCode": 15, - "http.statusCode": 15, - "request.headers.User-Agent": internal.MatchAnything, - "request.headers.userAgent": internal.MatchAnything, - "request.headers.contentType": "application/grpc", - "request.method": "TestApplication/DoUnaryUnaryError", - "request.uri": "grpc://bufnet/TestApplication/DoUnaryUnaryError", - }, - UserAttributes: map[string]interface{}{}, }}) } @@ -246,7 +233,7 @@ func TestUnaryStreamServerInterceptor(t *testing.T) { txn := app.StartTransaction("client") ctx := newrelic.NewContext(context.Background(), txn) stream, err := client.DoUnaryStream(ctx, &testapp.Message{}) - if nil != err { + if err != nil { t.Fatal("client call to DoUnaryStream failed", err) } var recved int @@ -255,7 +242,7 @@ func TestUnaryStreamServerInterceptor(t *testing.T) { if err == io.EOF { break } - if nil != err { + if err != nil { t.Fatal("error receiving message", err) } recved++ @@ -352,11 +339,11 @@ func TestStreamUnaryServerInterceptor(t *testing.T) { txn := app.StartTransaction("client") ctx := newrelic.NewContext(context.Background(), txn) stream, err := client.DoStreamUnary(ctx) - if nil != err { + if err != nil { t.Fatal("client call to DoStreamUnary failed", err) } for i := 0; i < 3; i++ { - if err := stream.Send(&testapp.Message{Text: "Hello DoStreamUnary"}); nil != err { + if err := stream.Send(&testapp.Message{Text: "Hello DoStreamUnary"}); err != nil { if err == io.EOF { break } @@ -364,7 +351,7 @@ func TestStreamUnaryServerInterceptor(t *testing.T) { } } _, err = stream.CloseAndRecv() - if nil != err { + if err != nil { t.Fatal("failure to CloseAndRecv", err) } @@ -456,7 +443,7 @@ func TestStreamStreamServerInterceptor(t *testing.T) { txn := app.StartTransaction("client") ctx := newrelic.NewContext(context.Background(), txn) stream, err := client.DoStreamStream(ctx) - if nil != err { + if err != nil { t.Fatal("client call to DoStreamStream failed", err) } waitc := make(chan struct{}) @@ -571,11 +558,11 @@ func TestStreamServerInterceptorError(t *testing.T) { client := testapp.NewTestApplicationClient(conn) stream, err := client.DoUnaryStreamError(context.Background(), &testapp.Message{}) - if nil != err { + if err != nil { t.Fatal("client call to DoUnaryStream failed", err) } _, err = stream.Recv() - if nil == err { + if err == nil { t.Fatal("DoUnaryStreamError should have returned an error") } @@ -604,10 +591,14 @@ func TestStreamServerInterceptorError(t *testing.T) { "sampled": internal.MatchAnything, "traceId": internal.MatchAnything, }, - UserAttributes: map[string]interface{}{}, + UserAttributes: map[string]interface{}{ + "GrpcStatusLevel": "error", + "GrpcStatusMessage": "oooooops!", + "GrpcStatusCode": "DataLoss", + }, AgentAttributes: map[string]interface{}{ - "httpResponseCode": 15, - "http.statusCode": 15, + "httpResponseCode": 0, + "http.statusCode": 0, "request.headers.contentType": "application/grpc", "request.method": "TestApplication/DoUnaryStreamError", "request.uri": "grpc://bufnet/TestApplication/DoUnaryStreamError", @@ -615,8 +606,8 @@ func TestStreamServerInterceptorError(t *testing.T) { }}) app.ExpectErrorEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ - "error.class": "15", - "error.message": "response code 15", + "error.class": "gRPC Error: DataLoss", + "error.message": "oooooops!", "guid": internal.MatchAnything, "priority": internal.MatchAnything, "sampled": internal.MatchAnything, @@ -625,36 +616,19 @@ func TestStreamServerInterceptorError(t *testing.T) { "transactionName": "WebTransaction/Go/TestApplication/DoUnaryStreamError", }, AgentAttributes: map[string]interface{}{ - "httpResponseCode": 15, - "http.statusCode": 15, + "httpResponseCode": 0, + "http.statusCode": 0, "request.headers.User-Agent": internal.MatchAnything, "request.headers.userAgent": internal.MatchAnything, "request.headers.contentType": "application/grpc", "request.method": "TestApplication/DoUnaryStreamError", "request.uri": "grpc://bufnet/TestApplication/DoUnaryStreamError", }, - UserAttributes: map[string]interface{}{}, - }, { - Intrinsics: map[string]interface{}{ - "error.class": internal.MatchAnything, - "error.message": "rpc error: code = DataLoss desc = oooooops!", - "guid": internal.MatchAnything, - "priority": internal.MatchAnything, - "sampled": internal.MatchAnything, - "spanId": internal.MatchAnything, - "traceId": internal.MatchAnything, - "transactionName": "WebTransaction/Go/TestApplication/DoUnaryStreamError", - }, - AgentAttributes: map[string]interface{}{ - "httpResponseCode": 15, - "http.statusCode": 15, - "request.headers.User-Agent": internal.MatchAnything, - "request.headers.userAgent": internal.MatchAnything, - "request.headers.contentType": "application/grpc", - "request.method": "TestApplication/DoUnaryStreamError", - "request.uri": "grpc://bufnet/TestApplication/DoUnaryStreamError", + UserAttributes: map[string]interface{}{ + "GrpcStatusLevel": "error", + "GrpcStatusMessage": "oooooops!", + "GrpcStatusCode": "DataLoss", }, - UserAttributes: map[string]interface{}{}, }}) } @@ -665,7 +639,7 @@ func TestUnaryServerInterceptorNilApp(t *testing.T) { client := testapp.NewTestApplicationClient(conn) msg, err := client.DoUnaryUnary(context.Background(), &testapp.Message{}) - if nil != err { + if err != nil { t.Fatal("unable to call client DoUnaryUnary", err) } if !strings.Contains(msg.Text, "content-type") { @@ -680,11 +654,11 @@ func TestStreamServerInterceptorNilApp(t *testing.T) { client := testapp.NewTestApplicationClient(conn) stream, err := client.DoStreamUnary(context.Background()) - if nil != err { + if err != nil { t.Fatal("client call to DoStreamUnary failed", err) } for i := 0; i < 3; i++ { - if err := stream.Send(&testapp.Message{Text: "Hello DoStreamUnary"}); nil != err { + if err := stream.Send(&testapp.Message{Text: "Hello DoStreamUnary"}); err != nil { if err == io.EOF { break } @@ -692,7 +666,7 @@ func TestStreamServerInterceptorNilApp(t *testing.T) { } } msg, err := stream.CloseAndRecv() - if nil != err { + if err != nil { t.Fatal("failure to CloseAndRecv", err) } if !strings.Contains(msg.Text, "content-type") { diff --git a/v3/newrelic/application.go b/v3/newrelic/application.go index fb81eb793..7a8db268f 100644 --- a/v3/newrelic/application.go +++ b/v3/newrelic/application.go @@ -83,6 +83,15 @@ func (app *Application) RecordCustomMetric(name string, value float64) { // If Infinite Tracing is enabled, WaitForConnection will block until a // connection to the Trace Observer is made, a fatal error is reached, or the // timeout is hit. +// +// Note that in most cases, it is not necesary nor recommended to call +// WaitForConnection() at all, particularly for any but the most trivial, short-lived +// processes. It is better to simply start the application and allow the +// instrumentation code to handle its connections on its own, which it will do +// as needed in the background (and will continue attempting to connect +// if it wasn't immediately successful, all while allowing your application +// to proceed with its primary function). +// func (app *Application) WaitForConnection(timeout time.Duration) error { if nil == app { return nil