Skip to content
This repository was archived by the owner on Sep 17, 2025. It is now read-only.

Commit 8a13b0a

Browse files
wkiserliyanhui1228
authored andcommitted
Update grpc server_interceptor to report accurate timing (#108)
1 parent 39299b8 commit 8a13b0a

File tree

2 files changed

+135
-33
lines changed

2 files changed

+135
-33
lines changed

opencensus/trace/ext/grpc/server_interceptor.py

Lines changed: 85 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -11,37 +11,87 @@
1111
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
14+
import collections
15+
import logging
1416

1517
import grpc
16-
import logging
1718

1819
from opencensus.trace import attributes_helper
1920
from opencensus.trace import tracer as tracer_module
21+
from opencensus.trace import execution_context
2022
from opencensus.trace.ext import grpc as oc_grpc
2123
from opencensus.trace.propagation import binary_format
2224

2325
ATTRIBUTE_COMPONENT = 'COMPONENT'
2426
ATTRIBUTE_ERROR_NAME = 'ERROR_NAME'
2527
ATTRIBUTE_ERROR_MESSAGE = 'ERROR_MESSAGE'
2628

29+
RpcRequestInfo = collections.namedtuple(
30+
'RPCRequestInfo', ('request', 'context')
31+
)
32+
RpcResponseInfo = collections.namedtuple(
33+
'RPCCallbackInfo', ('request', 'context', 'response', 'exc')
34+
)
35+
36+
37+
class RpcMethodHandlerWrapper(object):
38+
"""Wraps a grpc RPCMethodHandler and records the variables about the
39+
execution context and response
40+
"""
41+
42+
def __init__(
43+
self, handler, pre_handler_callbacks=None, post_handler_callbacks=None
44+
):
45+
"""
46+
:param handler: instance of RpcMethodHandler
47+
48+
:param pre_handler_callbacks: iterable of callbacks that accept an
49+
instance of RpcRequestInfo that are called before the server handler
50+
51+
:param post_handler_callbacks: iterable of callbacks that accept an
52+
instance of RpcResponseInfo that are called after the server
53+
handler finishes execution
54+
"""
55+
self.handler = handler
56+
self._pre_handler_callbacks = pre_handler_callbacks or []
57+
self._post_handler_callbacks = post_handler_callbacks or []
58+
59+
def proxy(self, prop_name):
60+
def _wrapper(request, context, *args, **kwargs):
61+
for callback in self._pre_handler_callbacks:
62+
callback(RpcRequestInfo(request, context))
63+
exc = None
64+
response = None
65+
try:
66+
response = getattr(
67+
self.handler, prop_name
68+
)(request, context, *args, **kwargs)
69+
except Exception as e:
70+
logging.error(e)
71+
exc = e
72+
raise
73+
finally:
74+
for callback in self._post_handler_callbacks:
75+
callback(RpcResponseInfo(request, context, response, exc))
76+
return response
77+
78+
return _wrapper
2779

28-
class OpenCensusServerInterceptor(grpc.ServerInterceptor):
80+
def __getattr__(self, item):
81+
if item in (
82+
'unary_unary', 'unary_stream', 'stream_unary', 'stream_stream'
83+
):
84+
return self.proxy(item)
85+
return getattr(self.handler, item)
2986

87+
88+
class OpenCensusServerInterceptor(grpc.ServerInterceptor):
3089
def __init__(self, sampler=None, exporter=None):
3190
self.sampler = sampler
3291
self.exporter = exporter
3392

34-
def _start_server_span(self, tracer):
35-
span = tracer.start_span(name='grpc_server')
36-
tracer.add_attribute_to_current_span(
37-
attribute_key=attributes_helper.COMMON_ATTRIBUTES.get(
38-
ATTRIBUTE_COMPONENT),
39-
attribute_value='grpc')
40-
41-
return span
42-
43-
def intercept_handler(self, continuation, handler_call_details):
44-
metadata = handler_call_details.invocation_metadata
93+
def _start_server_span(self, rpc_request_info):
94+
metadata = rpc_request_info.context.invocation_metadata()
4595
span_context = None
4696

4797
if metadata is not None:
@@ -55,21 +105,30 @@ def intercept_handler(self, continuation, handler_call_details):
55105
sampler=self.sampler,
56106
exporter=self.exporter)
57107

58-
with self._start_server_span(tracer):
59-
response = None
108+
span = tracer.start_span(name='grpc_server')
109+
tracer.add_attribute_to_current_span(
110+
attribute_key=attributes_helper.COMMON_ATTRIBUTES.get(
111+
ATTRIBUTE_COMPONENT),
112+
attribute_value='grpc')
60113

61-
try:
62-
response = continuation(handler_call_details)
63-
except Exception as e: # pragma: NO COVER
64-
logging.error(e)
65-
tracer.add_attribute_to_current_span(
66-
attributes_helper.COMMON_ATTRIBUTES.get(
67-
ATTRIBUTE_ERROR_MESSAGE),
68-
str(e))
69-
tracer.end_span()
70-
raise
114+
execution_context.set_opencensus_tracer(tracer)
115+
execution_context.set_current_span(span)
116+
117+
def _end_server_span(self, rpc_response_info):
118+
tracer = execution_context.get_opencensus_tracer()
119+
if rpc_response_info.exc is not None:
120+
tracer.add_attribute_to_current_span(
121+
attributes_helper.COMMON_ATTRIBUTES.get(
122+
ATTRIBUTE_ERROR_MESSAGE),
123+
str(rpc_response_info.exc))
124+
tracer.end_span()
71125

72-
return response
126+
def intercept_handler(self, continuation, handler_call_details):
127+
return RpcMethodHandlerWrapper(
128+
continuation(handler_call_details),
129+
pre_handler_callbacks=[self._start_server_span],
130+
post_handler_callbacks=[self._end_server_span]
131+
)
73132

74133
def intercept_service(self, continuation, handler_call_details):
75134
return self.intercept_handler(continuation, handler_call_details)

tests/unit/trace/ext/grpc/test_server_interceptor.py

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616

1717
import mock
1818

19-
from opencensus.trace.ext.grpc import server_interceptor
2019
from opencensus.trace import execution_context
20+
from opencensus.trace.ext.grpc import server_interceptor
2121

2222

2323
class TestOpenCensusServerInterceptor(unittest.TestCase):
@@ -29,19 +29,28 @@ def test_constructor(self):
2929
self.assertEqual(interceptor.sampler, sampler)
3030
self.assertEqual(interceptor.exporter, exporter)
3131

32+
def test_rpc_handler_wrapper(self):
33+
"""Ensure that RPCHandlerWrapper proxies to the unerlying handler"""
34+
mock_handler = mock.Mock()
35+
mock_handler.response_streaming = False
36+
wrapper = server_interceptor.RpcMethodHandlerWrapper(mock_handler)
37+
self.assertEqual(wrapper.response_streaming, False)
38+
3239
def test_intercept_handler_no_metadata(self):
3340
current_span = mock.Mock()
3441
mock_tracer = MockTracer(None, None, None)
3542
patch = mock.patch(
3643
'opencensus.trace.ext.grpc.server_interceptor.tracer_module.Tracer',
3744
MockTracer)
38-
mock_details = mock.Mock()
39-
mock_details.invocation_metadata = None
45+
mock_context = mock.Mock()
46+
mock_context.invocation_metadata = mock.Mock(return_value=None)
4047
interceptor = server_interceptor.OpenCensusServerInterceptor(
4148
None, None)
4249

4350
with patch:
44-
interceptor.intercept_handler(mock.Mock(), mock_details)
51+
interceptor.intercept_handler(
52+
mock.Mock(), mock.Mock()
53+
).unary_unary(mock.Mock(), mock_context)
4554

4655
expected_attributes = {
4756
'/component': 'grpc',
@@ -57,13 +66,17 @@ def test_intercept_handler(self):
5766
patch = mock.patch(
5867
'opencensus.trace.ext.grpc.server_interceptor.tracer_module.Tracer',
5968
MockTracer)
60-
mock_details = mock.Mock()
61-
mock_details.invocation_metadata = (('test_key', b'test_value'),)
69+
mock_context = mock.Mock()
70+
mock_context.invocation_metadata = mock.Mock(
71+
return_value=(('test_key', b'test_value'),)
72+
)
6273
interceptor = server_interceptor.OpenCensusServerInterceptor(
6374
None, None)
6475

6576
with patch:
66-
interceptor.intercept_handler(mock.Mock(), mock_details)
77+
interceptor.intercept_handler(
78+
mock.Mock(), mock.Mock()
79+
).unary_unary(mock.Mock(), mock_context)
6780

6881
expected_attributes = {
6982
'/component': 'grpc',
@@ -81,6 +94,36 @@ def test_intercept_service(self):
8194
interceptor.intercept_service(None, None)
8295
self.assertTrue(mock_handler.called)
8396

97+
def test_intercept_handler_exception(self):
98+
current_span = mock.Mock()
99+
mock_tracer = MockTracer(None, None, None)
100+
patch = mock.patch(
101+
'opencensus.trace.ext.grpc.server_interceptor.tracer_module.Tracer',
102+
MockTracer)
103+
interceptor = server_interceptor.OpenCensusServerInterceptor(
104+
None, None)
105+
mock_context = mock.Mock()
106+
mock_context.invocation_metadata = mock.Mock(return_value=None)
107+
mock_continuation = mock.Mock()
108+
mock_continuation.unary_unary = mock.Mock(side_effect=Exception('Test'))
109+
with patch:
110+
# patch the wrapper's handler to return an exception
111+
rpc_wrapper = interceptor.intercept_handler(
112+
mock.Mock(), mock.Mock())
113+
rpc_wrapper.handler.unary_unary = mock.Mock(
114+
side_effect=Exception('Test'))
115+
with self.assertRaises(Exception):
116+
rpc_wrapper.unary_unary(mock.Mock(), mock_context)
117+
118+
expected_attributes = {
119+
'/component': 'grpc',
120+
'/error/message': 'Test'
121+
}
122+
123+
self.assertEqual(
124+
execution_context.get_opencensus_tracer().current_span.attributes,
125+
expected_attributes)
126+
84127

85128
class MockTracer(object):
86129
def __init__(self, *args, **kwargs):

0 commit comments

Comments
 (0)