Skip to content

Commit f998e3f

Browse files
FaranIdohanyuancheungpellareddmathieuMrAlias
authored
Align gRPC server status code to span status code (#3685)
* implement based on new spec * fix return value of serverStatus * fix current tests * add internal error test * add changelog entry * move function to bottom * add ok test * add stream server test * refactor tests as table-driven tests, and add test for StreamServerInterceptor * Update CHANGELOG.md fix CR note on the changelog Co-authored-by: Robert Pająk <[email protected]> * refactor server tests to use shared assertion methods * fix more CR * add all gRPC status + remove name and grpcErr from vars --------- Co-authored-by: Chester Cheung <[email protected]> Co-authored-by: Robert Pająk <[email protected]> Co-authored-by: Damien Mathieu <[email protected]> Co-authored-by: Tyler Yahn <[email protected]>
1 parent 1870062 commit f998e3f

File tree

3 files changed

+191
-27
lines changed

3 files changed

+191
-27
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
1313
- AWS SDK add `rpc.system` attribute in `go.opentelemetry.io/contrib/instrumentation/github.com/aws/aws-sdk-go-v2/otelaws`. (#3582, #3617)
1414
- Add the new `go.opentelemetry.io/contrib/instrgen` package to provide auto-generated source code instrumentation. (#3068, #3108)
1515

16+
### Changed
17+
18+
- Update `go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc` to align gRPC server span status with the changes in the OpenTelemetry specification. (#3685)
19+
1620
### Fixed
1721

1822
- Prevent taking from reservoir in AWS XRay Remote Sampler when there is zero capacity in `go.opentelemetry.io/contrib/samplers/aws/xray`. (#3684)

instrumentation/google.golang.org/grpc/otelgrpc/interceptor.go

+27-3
Original file line numberDiff line numberDiff line change
@@ -342,8 +342,8 @@ func UnaryServerInterceptor(opts ...Option) grpc.UnaryServerInterceptor {
342342
resp, err := handler(ctx, req)
343343
if err != nil {
344344
s, _ := status.FromError(err)
345-
statusCode = s.Code()
346-
span.SetStatus(codes.Error, s.Message())
345+
statusCode, msg := serverStatus(s)
346+
span.SetStatus(statusCode, msg)
347347
span.SetAttributes(statusCodeAttr(s.Code()))
348348
messageSent.Event(ctx, 1, s.Proto())
349349
} else {
@@ -435,7 +435,8 @@ func StreamServerInterceptor(opts ...Option) grpc.StreamServerInterceptor {
435435
err := handler(srv, wrapServerStream(ctx, ss))
436436
if err != nil {
437437
s, _ := status.FromError(err)
438-
span.SetStatus(codes.Error, s.Message())
438+
statusCode, msg := serverStatus(s)
439+
span.SetStatus(statusCode, msg)
439440
span.SetAttributes(statusCodeAttr(s.Code()))
440441
} else {
441442
span.SetAttributes(statusCodeAttr(grpc_codes.OK))
@@ -499,3 +500,26 @@ func peerFromCtx(ctx context.Context) string {
499500
func statusCodeAttr(c grpc_codes.Code) attribute.KeyValue {
500501
return GRPCStatusCodeKey.Int64(int64(c))
501502
}
503+
504+
// serverStatus returns a span status code and message for a given gRPC
505+
// status code. It maps specific gRPC status codes to a corresponding span
506+
// status code and message. This function is intended for use on the server
507+
// side of a gRPC connection.
508+
//
509+
// If the gRPC status code is Unknown, DeadlineExceeded, Unimplemented,
510+
// Internal, Unavailable, or DataLoss, it returns a span status code of Error
511+
// and the message from the gRPC status. Otherwise, it returns a span status
512+
// code of Unset and an empty message.
513+
func serverStatus(grpcStatus *status.Status) (codes.Code, string) {
514+
switch grpcStatus.Code() {
515+
case grpc_codes.Unknown,
516+
grpc_codes.DeadlineExceeded,
517+
grpc_codes.Unimplemented,
518+
grpc_codes.Internal,
519+
grpc_codes.Unavailable,
520+
grpc_codes.DataLoss:
521+
return codes.Error, grpcStatus.Message()
522+
default:
523+
return codes.Unset, ""
524+
}
525+
}

instrumentation/google.golang.org/grpc/otelgrpc/test/interceptor_test.go

+160-24
Original file line numberDiff line numberDiff line change
@@ -583,39 +583,175 @@ func TestStreamClientInterceptorWithError(t *testing.T) {
583583
assert.Equal(t, codes.Error, span.Status().Code)
584584
}
585585

586-
func TestServerInterceptorError(t *testing.T) {
587-
sr := tracetest.NewSpanRecorder()
588-
tp := trace.NewTracerProvider(trace.WithSpanProcessor(sr))
589-
usi := otelgrpc.UnaryServerInterceptor(otelgrpc.WithTracerProvider(tp))
590-
deniedErr := status.Error(grpc_codes.PermissionDenied, "PERMISSION_DENIED_TEXT")
591-
handler := func(_ context.Context, _ interface{}) (interface{}, error) {
592-
return nil, deniedErr
593-
}
594-
_, err := usi(context.Background(), &grpc_testing.SimpleRequest{}, &grpc.UnaryServerInfo{}, handler)
595-
require.Error(t, err)
596-
assert.Equal(t, err, deniedErr)
586+
var serverChecks = []struct {
587+
grpcCode grpc_codes.Code
588+
wantSpanCode codes.Code
589+
wantSpanStatusDescription string
590+
}{
591+
{
592+
grpcCode: grpc_codes.OK,
593+
wantSpanCode: codes.Unset,
594+
wantSpanStatusDescription: "",
595+
},
596+
{
597+
grpcCode: grpc_codes.Canceled,
598+
wantSpanCode: codes.Unset,
599+
wantSpanStatusDescription: "",
600+
},
601+
{
602+
grpcCode: grpc_codes.Unknown,
603+
wantSpanCode: codes.Error,
604+
wantSpanStatusDescription: grpc_codes.Unknown.String(),
605+
},
606+
{
607+
grpcCode: grpc_codes.InvalidArgument,
608+
wantSpanCode: codes.Unset,
609+
wantSpanStatusDescription: "",
610+
},
611+
{
612+
grpcCode: grpc_codes.DeadlineExceeded,
613+
wantSpanCode: codes.Error,
614+
wantSpanStatusDescription: grpc_codes.DeadlineExceeded.String(),
615+
},
616+
{
617+
grpcCode: grpc_codes.NotFound,
618+
wantSpanCode: codes.Unset,
619+
wantSpanStatusDescription: "",
620+
},
621+
{
622+
grpcCode: grpc_codes.AlreadyExists,
623+
wantSpanCode: codes.Unset,
624+
wantSpanStatusDescription: "",
625+
},
626+
{
627+
grpcCode: grpc_codes.PermissionDenied,
628+
wantSpanCode: codes.Unset,
629+
wantSpanStatusDescription: "",
630+
},
631+
{
632+
grpcCode: grpc_codes.ResourceExhausted,
633+
wantSpanCode: codes.Unset,
634+
wantSpanStatusDescription: "",
635+
},
636+
{
637+
grpcCode: grpc_codes.FailedPrecondition,
638+
wantSpanCode: codes.Unset,
639+
wantSpanStatusDescription: "",
640+
},
641+
{
642+
grpcCode: grpc_codes.Aborted,
643+
wantSpanCode: codes.Unset,
644+
wantSpanStatusDescription: "",
645+
},
646+
{
647+
grpcCode: grpc_codes.OutOfRange,
648+
wantSpanCode: codes.Unset,
649+
wantSpanStatusDescription: "",
650+
},
651+
{
652+
grpcCode: grpc_codes.Unimplemented,
653+
wantSpanCode: codes.Error,
654+
wantSpanStatusDescription: grpc_codes.Unimplemented.String(),
655+
},
656+
{
657+
grpcCode: grpc_codes.Internal,
658+
wantSpanCode: codes.Error,
659+
wantSpanStatusDescription: grpc_codes.Internal.String(),
660+
},
661+
{
662+
grpcCode: grpc_codes.Unavailable,
663+
wantSpanCode: codes.Error,
664+
wantSpanStatusDescription: grpc_codes.Unavailable.String(),
665+
},
666+
{
667+
grpcCode: grpc_codes.DataLoss,
668+
wantSpanCode: codes.Error,
669+
wantSpanStatusDescription: grpc_codes.DataLoss.String(),
670+
},
671+
{
672+
grpcCode: grpc_codes.Unauthenticated,
673+
wantSpanCode: codes.Unset,
674+
wantSpanStatusDescription: "",
675+
},
676+
}
597677

598-
span, ok := getSpanFromRecorder(sr, "")
599-
if !ok {
600-
t.Fatalf("failed to export error span")
601-
}
602-
assert.Equal(t, codes.Error, span.Status().Code)
603-
assert.Contains(t, deniedErr.Error(), span.Status().Description)
678+
func assertServerSpan(t *testing.T, wantSpanCode codes.Code, wantSpanStatusDescription string, wantGrpcCode grpc_codes.Code, span trace.ReadOnlySpan) {
679+
// validate span status
680+
assert.Equal(t, wantSpanCode, span.Status().Code)
681+
assert.Equal(t, wantSpanStatusDescription, span.Status().Description)
682+
683+
// validate grpc code span attribute
604684
var codeAttr attribute.KeyValue
605685
for _, a := range span.Attributes() {
606686
if a.Key == otelgrpc.GRPCStatusCodeKey {
607687
codeAttr = a
608688
break
609689
}
610690
}
611-
if assert.True(t, codeAttr.Valid(), "attributes contain gRPC status code") {
612-
assert.Equal(t, attribute.Int64Value(int64(grpc_codes.PermissionDenied)), codeAttr.Value)
691+
692+
require.True(t, codeAttr.Valid(), "attributes contain gRPC status code")
693+
assert.Equal(t, attribute.Int64Value(int64(wantGrpcCode)), codeAttr.Value)
694+
}
695+
696+
// TestUnaryServerInterceptor tests the server interceptor for unary RPCs.
697+
func TestUnaryServerInterceptor(t *testing.T) {
698+
sr := tracetest.NewSpanRecorder()
699+
tp := trace.NewTracerProvider(trace.WithSpanProcessor(sr))
700+
usi := otelgrpc.UnaryServerInterceptor(otelgrpc.WithTracerProvider(tp))
701+
for _, check := range serverChecks {
702+
name := check.grpcCode.String()
703+
t.Run(name, func(t *testing.T) {
704+
// call the unary interceptor
705+
grpcErr := status.Error(check.grpcCode, check.grpcCode.String())
706+
handler := func(_ context.Context, _ interface{}) (interface{}, error) {
707+
return nil, grpcErr
708+
}
709+
_, err := usi(context.Background(), &grpc_testing.SimpleRequest{}, &grpc.UnaryServerInfo{FullMethod: name}, handler)
710+
assert.Equal(t, grpcErr, err)
711+
712+
// validate span
713+
span, ok := getSpanFromRecorder(sr, name)
714+
require.True(t, ok, "missing span %s", name)
715+
assertServerSpan(t, check.wantSpanCode, check.wantSpanStatusDescription, check.grpcCode, span)
716+
717+
// validate events and their attributes
718+
assert.Len(t, span.Events(), 2)
719+
assert.ElementsMatch(t, []attribute.KeyValue{
720+
attribute.Key("message.type").String("SENT"),
721+
attribute.Key("message.id").Int(1),
722+
}, span.Events()[1].Attributes)
723+
})
724+
}
725+
}
726+
727+
type mockServerStream struct {
728+
grpc.ServerStream
729+
}
730+
731+
func (m *mockServerStream) Context() context.Context { return context.Background() }
732+
733+
// TestStreamServerInterceptor tests the server interceptor for streaming RPCs.
734+
func TestStreamServerInterceptor(t *testing.T) {
735+
sr := tracetest.NewSpanRecorder()
736+
tp := trace.NewTracerProvider(trace.WithSpanProcessor(sr))
737+
usi := otelgrpc.StreamServerInterceptor(otelgrpc.WithTracerProvider(tp))
738+
for _, check := range serverChecks {
739+
name := check.grpcCode.String()
740+
t.Run(name, func(t *testing.T) {
741+
// call the stream interceptor
742+
grpcErr := status.Error(check.grpcCode, check.grpcCode.String())
743+
handler := func(_ interface{}, _ grpc.ServerStream) error {
744+
return grpcErr
745+
}
746+
err := usi(&grpc_testing.SimpleRequest{}, &mockServerStream{}, &grpc.StreamServerInfo{FullMethod: name}, handler)
747+
assert.Equal(t, grpcErr, err)
748+
749+
// validate span
750+
span, ok := getSpanFromRecorder(sr, name)
751+
require.True(t, ok, "missing span %s", name)
752+
assertServerSpan(t, check.wantSpanCode, check.wantSpanStatusDescription, check.grpcCode, span)
753+
})
613754
}
614-
assert.Len(t, span.Events(), 2)
615-
assert.ElementsMatch(t, []attribute.KeyValue{
616-
attribute.Key("message.type").String("SENT"),
617-
attribute.Key("message.id").Int(1),
618-
}, span.Events()[1].Attributes)
619755
}
620756

621757
func TestParseFullMethod(t *testing.T) {

0 commit comments

Comments
 (0)