Repository: DataDog/dd-trace-py

Issue:
gRPC server interceptor raises AttributeError
When a gRPC service is not found, the Datadog interceptor raises an AttributeError:

```
ERROR    grpc._server:_server.py:732 Exception servicing handler: 'NoneType' object has no attribute '_pin'
...
  File "/src/ddtrace/contrib/grpc/server_interceptor.py", line 153, in intercept_service
    return self._fn(continuation, handler_call_details)
  File "/src/ddtrace/contrib/grpc/server_interceptor.py", line 22, in interceptor_function
    return _TracedRpcMethodHandler(pin, handler_call_details, rpc_method_handler)
  File "/src/ddtrace/contrib/grpc/server_interceptor.py", line 58, in __init__
    self._pin = pin
AttributeError: 'NoneType' object has no attribute '_pin'
```

The gRPC documentation [points](https://grpc.github.io/grpc/python/grpc.html#grpc.ServerInterceptor.intercept_service) out that `continuation()` in a server-side interceptor can return a `None`, which is not handled by the Datadog interceptor.

This applies to `ddtrace>=0.28.0` when the server interceptor was added.


Discussion:

Resolving patch:
diff --git a/ddtrace/contrib/grpc/server_interceptor.py b/ddtrace/contrib/grpc/server_interceptor.py
--- a/ddtrace/contrib/grpc/server_interceptor.py
+++ b/ddtrace/contrib/grpc/server_interceptor.py
@@ -18,7 +18,15 @@ def interceptor_function(continuation, handler_call_details):
             return continuation(handler_call_details)
 
         rpc_method_handler = continuation(handler_call_details)
-        return _TracedRpcMethodHandler(pin, handler_call_details, rpc_method_handler)
+
+        # continuation returns an RpcMethodHandler instance if the RPC is
+        # considered serviced, or None otherwise
+        # https://grpc.github.io/grpc/python/grpc.html#grpc.ServerInterceptor.intercept_service
+
+        if rpc_method_handler:
+            return _TracedRpcMethodHandler(pin, handler_call_details, rpc_method_handler)
+
+        return rpc_method_handler
 
     return _ServerInterceptor(interceptor_function)
 


Test patch:
diff --git a/tests/contrib/grpc/hello_pb2_grpc.py b/tests/contrib/grpc/hello_pb2_grpc.py
--- a/tests/contrib/grpc/hello_pb2_grpc.py
+++ b/tests/contrib/grpc/hello_pb2_grpc.py
@@ -35,6 +35,11 @@ def __init__(self, channel):
         request_serializer=hello__pb2.HelloRequest.SerializeToString,
         response_deserializer=hello__pb2.HelloReply.FromString,
         )
+    self.SayHelloUnknown = channel.unary_unary(
+        '/helloworld.Hello/SayHelloUnknown',
+        request_serializer=hello__pb2.HelloRequest.SerializeToString,
+        response_deserializer=hello__pb2.HelloReply.FromString,
+        )
 
 
 class HelloServicer(object):
@@ -69,6 +74,12 @@ def SayHelloLast(self, request_iterator, context):
     context.set_details('Method not implemented!')
     raise NotImplementedError('Method not implemented!')
 
+  def SayHelloUnknown(self, request_iterator, context):
+    # missing associated documentation comment in .proto file
+    pass
+    context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+    context.set_details('Method not implemented!')
+    raise NotImplementedError('Method not implemented!')
 
 def add_HelloServicer_to_server(servicer, server):
   rpc_method_handlers = {
diff --git a/tests/contrib/grpc/test_grpc.py b/tests/contrib/grpc/test_grpc.py
--- a/tests/contrib/grpc/test_grpc.py
+++ b/tests/contrib/grpc/test_grpc.py
@@ -440,6 +440,14 @@ def test_server_stream_exception(self):
         assert 'Traceback' in server_span.get_tag(errors.ERROR_STACK)
         assert 'grpc.StatusCode.RESOURCE_EXHAUSTED' in server_span.get_tag(errors.ERROR_STACK)
 
+    def test_unknown_servicer(self):
+        with grpc.secure_channel('localhost:%d' % (_GRPC_PORT), credentials=grpc.ChannelCredentials(None)) as channel:
+            stub = HelloStub(channel)
+            with self.assertRaises(grpc.RpcError) as exception_context:
+                stub.SayHelloUnknown(HelloRequest(name='unknown'))
+            rpc_error = exception_context.exception
+            assert grpc.StatusCode.UNIMPLEMENTED == rpc_error.code()
+
     @BaseTracerTestCase.run_in_subprocess(env_overrides=dict(DD_SERVICE="mysvc"))
     def test_server_service_name(self):
         """
@@ -515,6 +523,9 @@ def SayHelloRepeatedly(self, request_iterator, context):
         if last_request is not None:
             yield HelloReply(message='{}'.format(last_request.name))
 
+    def SayHelloUnknown(self, request, context):
+        yield HelloReply(message='unknown')
+
 
 class _CustomException(Exception):
     pass