Skip to content

Commit 62964ec

Browse files
committed
fix grpc host issue and add grpc test cases
1 parent c6bb9ee commit 62964ec

2 files changed

Lines changed: 359 additions & 1 deletion

File tree

elasticapm/instrumentation/packages/grpc.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ def call(self, module, method, wrapped, instance, args, kwargs):
5454
except ValueError:
5555
port = None
5656
else:
57-
host, port = None, None
57+
host, port = target, None
5858
return grpc.intercept_channel(result, _ClientInterceptor(host, port, secure=method == "secure_channel"))
5959

6060

Lines changed: 358 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,358 @@
1+
# BSD 3-Clause License
2+
#
3+
# Copyright (c) 2022, Elasticsearch BV
4+
# All rights reserved.
5+
#
6+
# Redistribution and use in source and binary forms, with or without
7+
# modification, are permitted provided that the following conditions are met:
8+
#
9+
# * Redistributions of source code must retain the above copyright notice, this
10+
# list of conditions and the following disclaimer.
11+
#
12+
# * Redistributions in binary form must reproduce the above copyright notice,
13+
# this list of conditions and the following disclaimer in the documentation
14+
# and/or other materials provided with the distribution.
15+
#
16+
# * Neither the name of the copyright holder nor the names of its
17+
# contributors may be used to endorse or promote products derived from
18+
# this software without specific prior written permission.
19+
#
20+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24+
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25+
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26+
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27+
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28+
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30+
31+
import pytest
32+
33+
grpc = pytest.importorskip("grpc")
34+
35+
from unittest.mock import MagicMock, patch, call
36+
37+
pytestmark = pytest.mark.grpc
38+
39+
from elasticapm.instrumentation.packages.grpc import (
40+
GRPCClientInstrumentation,
41+
GRPCServerInstrumentation,
42+
GRPCAsyncServerInstrumentation,
43+
)
44+
45+
46+
# ---------------------------------------------------------------------------
47+
# Helpers
48+
# ---------------------------------------------------------------------------
49+
50+
def _make_client_instrumentation():
51+
return GRPCClientInstrumentation()
52+
53+
54+
def _make_server_instrumentation():
55+
return GRPCServerInstrumentation()
56+
57+
58+
def _make_async_server_instrumentation():
59+
return GRPCAsyncServerInstrumentation()
60+
61+
62+
# ---------------------------------------------------------------------------
63+
# GRPCClientInstrumentation
64+
# ---------------------------------------------------------------------------
65+
66+
67+
class TestGRPCClientInstrumentation:
68+
def test_insecure_channel_positional_arg_parses_host_port(self):
69+
"""insecure_channel called with 'host:port' as positional arg."""
70+
instrumentation = _make_client_instrumentation()
71+
fake_channel = MagicMock()
72+
wrapped = MagicMock(return_value=fake_channel)
73+
intercepted_channel = MagicMock()
74+
75+
with patch("grpc.intercept_channel", return_value=intercepted_channel) as mock_intercept, \
76+
patch("elasticapm.contrib.grpc.client_interceptor._ClientInterceptor") as MockInterceptor:
77+
result = instrumentation.call(
78+
module="grpc",
79+
method="insecure_channel",
80+
wrapped=wrapped,
81+
instance=None,
82+
args=("myhost:50051",),
83+
kwargs={},
84+
)
85+
86+
wrapped.assert_called_once_with("myhost:50051")
87+
MockInterceptor.assert_called_once_with("myhost", 50051, secure=False)
88+
mock_intercept.assert_called_once_with(fake_channel, MockInterceptor.return_value)
89+
assert result is intercepted_channel
90+
91+
def test_insecure_channel_keyword_arg_parses_host_port(self):
92+
"""insecure_channel called with target= kwarg."""
93+
instrumentation = _make_client_instrumentation()
94+
fake_channel = MagicMock()
95+
wrapped = MagicMock(return_value=fake_channel)
96+
97+
with patch("grpc.intercept_channel") as mock_intercept, \
98+
patch("elasticapm.contrib.grpc.client_interceptor._ClientInterceptor") as MockInterceptor:
99+
instrumentation.call(
100+
module="grpc",
101+
method="insecure_channel",
102+
wrapped=wrapped,
103+
instance=None,
104+
args=(),
105+
kwargs={"target": "myhost:8080"},
106+
)
107+
108+
MockInterceptor.assert_called_once_with("myhost", 8080, secure=False)
109+
110+
def test_secure_channel_sets_secure_flag(self):
111+
"""secure_channel passes secure=True to _ClientInterceptor."""
112+
instrumentation = _make_client_instrumentation()
113+
wrapped = MagicMock(return_value=MagicMock())
114+
115+
with patch("grpc.intercept_channel"), \
116+
patch("elasticapm.contrib.grpc.client_interceptor._ClientInterceptor") as MockInterceptor:
117+
instrumentation.call(
118+
module="grpc",
119+
method="secure_channel",
120+
wrapped=wrapped,
121+
instance=None,
122+
args=("myhost:443",),
123+
kwargs={},
124+
)
125+
126+
MockInterceptor.assert_called_once_with("myhost", 443, secure=True)
127+
128+
def test_host_without_port(self):
129+
"""Target with no colon produces port=None."""
130+
instrumentation = _make_client_instrumentation()
131+
wrapped = MagicMock(return_value=MagicMock())
132+
133+
with patch("grpc.intercept_channel"), \
134+
patch("elasticapm.contrib.grpc.client_interceptor._ClientInterceptor") as MockInterceptor:
135+
instrumentation.call(
136+
module="grpc",
137+
method="insecure_channel",
138+
wrapped=wrapped,
139+
instance=None,
140+
args=("myhost",),
141+
kwargs={},
142+
)
143+
144+
MockInterceptor.assert_called_once_with("myhost", None, secure=False)
145+
146+
def test_non_integer_port_becomes_none(self):
147+
"""If port segment is not a valid integer, port is set to None."""
148+
instrumentation = _make_client_instrumentation()
149+
wrapped = MagicMock(return_value=MagicMock())
150+
151+
with patch("grpc.intercept_channel"), \
152+
patch("elasticapm.contrib.grpc.client_interceptor._ClientInterceptor") as MockInterceptor:
153+
instrumentation.call(
154+
module="grpc",
155+
method="insecure_channel",
156+
wrapped=wrapped,
157+
instance=None,
158+
args=("myhost:notaport",),
159+
kwargs={},
160+
)
161+
162+
MockInterceptor.assert_called_once_with("myhost", None, secure=False)
163+
164+
def test_returns_intercepted_channel(self):
165+
"""The return value is whatever grpc.intercept_channel returns."""
166+
instrumentation = _make_client_instrumentation()
167+
sentinel = MagicMock()
168+
wrapped = MagicMock(return_value=MagicMock())
169+
170+
with patch("grpc.intercept_channel", return_value=sentinel), \
171+
patch("elasticapm.contrib.grpc.client_interceptor._ClientInterceptor"):
172+
result = instrumentation.call(
173+
module="grpc",
174+
method="insecure_channel",
175+
wrapped=wrapped,
176+
instance=None,
177+
args=("host:1234",),
178+
kwargs={},
179+
)
180+
181+
assert result is sentinel
182+
183+
184+
# ---------------------------------------------------------------------------
185+
# GRPCServerInstrumentation
186+
# ---------------------------------------------------------------------------
187+
188+
189+
class TestGRPCServerInstrumentation:
190+
def test_no_interceptors_adds_server_interceptor_via_kwargs(self):
191+
"""With no existing interceptors, _ServerInterceptor is added via kwargs."""
192+
instrumentation = _make_server_instrumentation()
193+
fake_server = MagicMock()
194+
wrapped = MagicMock(return_value=fake_server)
195+
196+
with patch("elasticapm.contrib.grpc.server_interceptor._ServerInterceptor") as MockInterceptor:
197+
result = instrumentation.call(
198+
module="grpc",
199+
method="server",
200+
wrapped=wrapped,
201+
instance=None,
202+
args=(),
203+
kwargs={},
204+
)
205+
206+
interceptors_passed = wrapped.call_args.kwargs["interceptors"]
207+
assert interceptors_passed[0] is MockInterceptor.return_value
208+
assert result is fake_server
209+
210+
def test_existing_interceptors_via_kwargs_prepends_server_interceptor(self):
211+
"""_ServerInterceptor is inserted at index 0, before existing interceptors."""
212+
instrumentation = _make_server_instrumentation()
213+
existing = MagicMock()
214+
wrapped = MagicMock(return_value=MagicMock())
215+
216+
with patch("elasticapm.contrib.grpc.server_interceptor._ServerInterceptor") as MockInterceptor:
217+
instrumentation.call(
218+
module="grpc",
219+
method="server",
220+
wrapped=wrapped,
221+
instance=None,
222+
args=(),
223+
kwargs={"interceptors": [existing]},
224+
)
225+
226+
interceptors_passed = wrapped.call_args.kwargs["interceptors"]
227+
assert interceptors_passed[0] is MockInterceptor.return_value
228+
assert interceptors_passed[1] is existing
229+
230+
def test_existing_interceptors_via_positional_args(self):
231+
"""_ServerInterceptor is prepended when interceptors are in args[2]."""
232+
instrumentation = _make_server_instrumentation()
233+
existing = MagicMock()
234+
thread_pool = MagicMock()
235+
wrapped = MagicMock(return_value=MagicMock())
236+
237+
# args[0]=thread_pool, args[1]=options, args[2]=interceptors
238+
with patch("elasticapm.contrib.grpc.server_interceptor._ServerInterceptor") as MockInterceptor:
239+
instrumentation.call(
240+
module="grpc",
241+
method="server",
242+
wrapped=wrapped,
243+
instance=None,
244+
args=(thread_pool, None, [existing]),
245+
kwargs={},
246+
)
247+
248+
call_args = wrapped.call_args.args
249+
interceptors_passed = call_args[2]
250+
assert interceptors_passed[0] is MockInterceptor.return_value
251+
assert interceptors_passed[1] is existing
252+
253+
def test_no_interceptors_in_positional_args_uses_kwargs(self):
254+
"""When args has fewer than 3 elements, interceptors go to kwargs."""
255+
instrumentation = _make_server_instrumentation()
256+
thread_pool = MagicMock()
257+
wrapped = MagicMock(return_value=MagicMock())
258+
259+
with patch("elasticapm.contrib.grpc.server_interceptor._ServerInterceptor") as MockInterceptor:
260+
instrumentation.call(
261+
module="grpc",
262+
method="server",
263+
wrapped=wrapped,
264+
instance=None,
265+
args=(thread_pool,),
266+
kwargs={},
267+
)
268+
269+
interceptors_passed = wrapped.call_args.kwargs["interceptors"]
270+
assert interceptors_passed[0] is MockInterceptor.return_value
271+
272+
273+
# ---------------------------------------------------------------------------
274+
# GRPCAsyncServerInstrumentation
275+
# ---------------------------------------------------------------------------
276+
277+
278+
class TestGRPCAsyncServerInstrumentation:
279+
def test_no_interceptors_adds_async_interceptor_via_kwargs(self):
280+
"""With no existing interceptors, _AsyncServerInterceptor is added via kwargs."""
281+
instrumentation = _make_async_server_instrumentation()
282+
fake_server = MagicMock()
283+
wrapped = MagicMock(return_value=fake_server)
284+
285+
with patch("elasticapm.contrib.grpc.async_server_interceptor._AsyncServerInterceptor") as MockInterceptor:
286+
result = instrumentation.call(
287+
module="grpc.aio",
288+
method="server",
289+
wrapped=wrapped,
290+
instance=None,
291+
args=(),
292+
kwargs={},
293+
)
294+
295+
interceptors_passed = wrapped.call_args.kwargs["interceptors"]
296+
assert interceptors_passed[0] is MockInterceptor.return_value
297+
assert result is fake_server
298+
299+
def test_existing_interceptors_via_kwargs_prepends_async_interceptor(self):
300+
"""_AsyncServerInterceptor is inserted at index 0 before existing interceptors."""
301+
instrumentation = _make_async_server_instrumentation()
302+
existing = MagicMock()
303+
wrapped = MagicMock(return_value=MagicMock())
304+
305+
with patch("elasticapm.contrib.grpc.async_server_interceptor._AsyncServerInterceptor") as MockInterceptor:
306+
instrumentation.call(
307+
module="grpc.aio",
308+
method="server",
309+
wrapped=wrapped,
310+
instance=None,
311+
args=(),
312+
kwargs={"interceptors": [existing]},
313+
)
314+
315+
interceptors_passed = wrapped.call_args.kwargs["interceptors"]
316+
assert interceptors_passed[0] is MockInterceptor.return_value
317+
assert interceptors_passed[1] is existing
318+
319+
def test_existing_interceptors_via_positional_args(self):
320+
"""_AsyncServerInterceptor is prepended when interceptors are in args[2]."""
321+
instrumentation = _make_async_server_instrumentation()
322+
existing = MagicMock()
323+
thread_pool = MagicMock()
324+
wrapped = MagicMock(return_value=MagicMock())
325+
326+
with patch("elasticapm.contrib.grpc.async_server_interceptor._AsyncServerInterceptor") as MockInterceptor:
327+
instrumentation.call(
328+
module="grpc.aio",
329+
method="server",
330+
wrapped=wrapped,
331+
instance=None,
332+
args=(thread_pool, None, [existing]),
333+
kwargs={},
334+
)
335+
336+
call_args = wrapped.call_args.args
337+
interceptors_passed = call_args[2]
338+
assert interceptors_passed[0] is MockInterceptor.return_value
339+
assert interceptors_passed[1] is existing
340+
341+
def test_no_interceptors_in_short_positional_args_uses_kwargs(self):
342+
"""When args has fewer than 3 elements, interceptors go to kwargs."""
343+
instrumentation = _make_async_server_instrumentation()
344+
thread_pool = MagicMock()
345+
wrapped = MagicMock(return_value=MagicMock())
346+
347+
with patch("elasticapm.contrib.grpc.async_server_interceptor._AsyncServerInterceptor") as MockInterceptor:
348+
instrumentation.call(
349+
module="grpc.aio",
350+
method="server",
351+
wrapped=wrapped,
352+
instance=None,
353+
args=(thread_pool,),
354+
kwargs={},
355+
)
356+
357+
interceptors_passed = wrapped.call_args.kwargs["interceptors"]
358+
assert interceptors_passed[0] is MockInterceptor.return_value

0 commit comments

Comments
 (0)