From c341091997744111c8b194ed95c711397c5caed5 Mon Sep 17 00:00:00 2001 From: "ali.sorouramini" Date: Fri, 28 Apr 2023 16:07:45 +0330 Subject: [PATCH 01/45] feat: Add interceptor for async gRPC server --- sentry_sdk/integrations/grpc/aio/__init__.py | 1 + sentry_sdk/integrations/grpc/aio/server.py | 61 ++++++++++ tests/integrations/grpc/test_aio_grpc.py | 119 +++++++++++++++++++ 3 files changed, 181 insertions(+) create mode 100644 sentry_sdk/integrations/grpc/aio/__init__.py create mode 100644 sentry_sdk/integrations/grpc/aio/server.py create mode 100644 tests/integrations/grpc/test_aio_grpc.py diff --git a/sentry_sdk/integrations/grpc/aio/__init__.py b/sentry_sdk/integrations/grpc/aio/__init__.py new file mode 100644 index 0000000000..c3695e948e --- /dev/null +++ b/sentry_sdk/integrations/grpc/aio/__init__.py @@ -0,0 +1 @@ +from .server import ServerInterceptor # noqa: F401 diff --git a/sentry_sdk/integrations/grpc/aio/server.py b/sentry_sdk/integrations/grpc/aio/server.py new file mode 100644 index 0000000000..8b15ca34f6 --- /dev/null +++ b/sentry_sdk/integrations/grpc/aio/server.py @@ -0,0 +1,61 @@ +from functools import wraps + +from sentry_sdk import Hub +from sentry_sdk._types import MYPY +from sentry_sdk.consts import OP +from sentry_sdk.integrations import DidNotEnable +from sentry_sdk.tracing import Transaction, TRANSACTION_SOURCE_CUSTOM + +if MYPY: + from collections.abc import Awaitable, Callable + from typing import Any + + +try: + import grpc + from grpc import HandlerCallDetails, RpcMethodHandler + from grpc.aio import ServicerContext + from grpc.experimental import wrap_server_method_handler +except ImportError: + raise DidNotEnable("grpcio is not installed") + + +class ServerInterceptor(grpc.aio.ServerInterceptor): # type: ignore + def __init__(self, find_name=None): + # type: (ServerInterceptor, Callable[[ServicerContext], str] | None) -> None + self._find_method_name = find_name or ServerInterceptor._find_name + + super(ServerInterceptor, self).__init__() + + async def intercept_service(self, continuation, handler_call_details): + # type: (ServerInterceptor, Callable[[HandlerCallDetails], Awaitable[RpcMethodHandler]], HandlerCallDetails) -> Awaitable[RpcMethodHandler] + handler = await continuation(handler_call_details) + return wrap_server_method_handler(self.wrapper, handler) + + def wrapper(self, handler): + # type: (ServerInterceptor, Callable[[Any, ServicerContext], Awaitable[Any]]) -> Callable[[Any, ServicerContext], Awaitable[Any]] + @wraps(handler) + async def wrapped(request, context): + # type: (Any, ServicerContext) -> Any + name = self._find_method_name(context) + if not name: + return await handler(request, context) + + hub = Hub(Hub.current) + + transaction = Transaction.continue_from_headers( + dict(context.invocation_metadata()), + op=OP.GRPC_SERVER, + name=name, + source=TRANSACTION_SOURCE_CUSTOM, + ) + + with hub.start_transaction(transaction=transaction): + return await handler(request, context) + + return wrapped + + @staticmethod + def _find_name(context): + # type: (ServicerContext) -> str + return context._rpc_event.call_details.method.decode() diff --git a/tests/integrations/grpc/test_aio_grpc.py b/tests/integrations/grpc/test_aio_grpc.py new file mode 100644 index 0000000000..1fce43a9b5 --- /dev/null +++ b/tests/integrations/grpc/test_aio_grpc.py @@ -0,0 +1,119 @@ +from __future__ import absolute_import + +import asyncio +import os + +import grpc +import pytest +import pytest_asyncio +import sentry_sdk + +from sentry_sdk import Hub +from sentry_sdk.consts import OP +from sentry_sdk.integrations.grpc.aio.server import ServerInterceptor +from tests.integrations.grpc.grpc_test_service_pb2 import gRPCTestMessage +from tests.integrations.grpc.grpc_test_service_pb2_grpc import ( + gRPCTestServiceServicer, + add_gRPCTestServiceServicer_to_server, + gRPCTestServiceStub, +) + +AIO_PORT = 50052 +AIO_PORT += os.getpid() % 100 # avoid port conflicts when running tests in parallel + + +@pytest.fixture(scope="function") +def event_loop(request): + """Create an instance of the default event loop for each test case.""" + loop = asyncio.new_event_loop() + yield loop + loop.close() + + +@pytest_asyncio.fixture(scope="function") +async def grpc_server(event_loop): + server = grpc.aio.server( + interceptors=[ServerInterceptor(find_name=lambda request: request.__class__)] + ) + server.add_insecure_port(f"[::]:{AIO_PORT}") + add_gRPCTestServiceServicer_to_server(TestService, server) + + await event_loop.create_task(server.start()) + + try: + yield server + finally: + await server.stop(None) + + +@pytest.mark.asyncio +async def test_grpc_server_starts_transaction(sentry_init, capture_events, grpc_server): + sentry_init(traces_sample_rate=1.0) + events = capture_events() + + async with grpc.aio.insecure_channel(f"localhost:{AIO_PORT}") as channel: + stub = gRPCTestServiceStub(channel) + await stub.TestServe(gRPCTestMessage(text="test")) + + (event,) = events + span = event["spans"][0] + + assert event["type"] == "transaction" + assert event["transaction_info"] == { + "source": "custom", + } + assert event["contexts"]["trace"]["op"] == OP.GRPC_SERVER + assert span["op"] == "test" + + +@pytest.mark.asyncio +async def test_grpc_server_continues_transaction( + sentry_init, capture_events, grpc_server +): + sentry_init(traces_sample_rate=1.0) + events = capture_events() + + async with grpc.aio.insecure_channel(f"localhost:{AIO_PORT}") as channel: + stub = gRPCTestServiceStub(channel) + + with sentry_sdk.start_transaction() as transaction: + metadata = ( + ( + "baggage", + "sentry-trace_id={trace_id},sentry-environment=test," + "sentry-transaction=test-transaction,sentry-sample_rate=1.0".format( + trace_id=transaction.trace_id + ), + ), + ( + "sentry-trace", + "{trace_id}-{parent_span_id}-{sampled}".format( + trace_id=transaction.trace_id, + parent_span_id=transaction.span_id, + sampled=1, + ), + ), + ) + + await stub.TestServe(gRPCTestMessage(text="test"), metadata=metadata) + + (event, _) = events + span = event["spans"][0] + + assert event["type"] == "transaction" + assert event["transaction_info"] == { + "source": "custom", + } + assert event["contexts"]["trace"]["op"] == OP.GRPC_SERVER + assert event["contexts"]["trace"]["trace_id"] == transaction.trace_id + assert span["op"] == "test" + + +class TestService(gRPCTestServiceServicer): + @staticmethod + async def TestServe(request, context): # noqa: N802 + hub = Hub.current + with hub.start_span(op="test", description="test"): + pass + + return gRPCTestMessage(text=request.text) From a554078e0f16f1503044c6001813c64c9b9d8076 Mon Sep 17 00:00:00 2001 From: "ali.sorouramini" Date: Fri, 28 Apr 2023 23:00:33 +0330 Subject: [PATCH 02/45] feat: Add exception handling to async gRPC interceptor --- sentry_sdk/integrations/grpc/aio/server.py | 11 +++++++- tests/integrations/grpc/test_aio_grpc.py | 32 ++++++++++++++++++++-- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/sentry_sdk/integrations/grpc/aio/server.py b/sentry_sdk/integrations/grpc/aio/server.py index 8b15ca34f6..34554d866f 100644 --- a/sentry_sdk/integrations/grpc/aio/server.py +++ b/sentry_sdk/integrations/grpc/aio/server.py @@ -5,6 +5,7 @@ from sentry_sdk.consts import OP from sentry_sdk.integrations import DidNotEnable from sentry_sdk.tracing import Transaction, TRANSACTION_SOURCE_CUSTOM +from sentry_sdk.utils import event_from_exception if MYPY: from collections.abc import Awaitable, Callable @@ -51,7 +52,15 @@ async def wrapped(request, context): ) with hub.start_transaction(transaction=transaction): - return await handler(request, context) + try: + return await handler(request, context) + except Exception as exc: + event, hint = event_from_exception( + exc, + mechanism={"type": "gRPC", "handled": False}, + ) + hub.capture_event(event, hint=hint) + raise return wrapped diff --git a/tests/integrations/grpc/test_aio_grpc.py b/tests/integrations/grpc/test_aio_grpc.py index 1fce43a9b5..0337647363 100644 --- a/tests/integrations/grpc/test_aio_grpc.py +++ b/tests/integrations/grpc/test_aio_grpc.py @@ -109,11 +109,39 @@ async def test_grpc_server_continues_transaction( assert span["op"] == "test" +@pytest.mark.asyncio +async def test_grpc_server_exception(sentry_init, capture_events, grpc_server): + sentry_init(traces_sample_rate=1.0) + events = capture_events() + + async with grpc.aio.insecure_channel(f"localhost:{AIO_PORT}") as channel: + stub = gRPCTestServiceStub(channel) + try: + await stub.TestServe(gRPCTestMessage(text="exception")) + raise AssertionError() + except Exception: + pass + + (event, _) = events + + assert event["exception"]["values"][0]["type"] == "TestService.TestException" + assert event["exception"]["values"][0]["value"] == "test" + assert event["exception"]["values"][0]["mechanism"]["handled"] is False + assert event["exception"]["values"][0]["mechanism"]["type"] == "gRPC" + + class TestService(gRPCTestServiceServicer): - @staticmethod - async def TestServe(request, context): # noqa: N802 + class TestException(Exception): + def __init__(self): + super().__init__("test") + + @classmethod + async def TestServe(cls, request, context): # noqa: N802 hub = Hub.current with hub.start_span(op="test", description="test"): pass + if request.text == "exception": + raise cls.TestException() + return gRPCTestMessage(text=request.text) From 0296157965b51c176a5ca67bee4ff9e34afbf6ed Mon Sep 17 00:00:00 2001 From: Florian Dellekart Date: Sat, 26 Aug 2023 14:32:37 +0200 Subject: [PATCH 03/45] feat: Add gRPC integration with monkeypatch for synchronous client side channels The integration monkeypatches the functions used to create channels on the client side so the channel does not have to be explicitly intercepted by users. --- sentry_sdk/integrations/grpc/__init__.py | 37 ++++++++++++++++++++++-- tests/integrations/grpc/test_grpc.py | 10 ++----- 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/sentry_sdk/integrations/grpc/__init__.py b/sentry_sdk/integrations/grpc/__init__.py index 59bfd502e5..21b3cbbb6e 100644 --- a/sentry_sdk/integrations/grpc/__init__.py +++ b/sentry_sdk/integrations/grpc/__init__.py @@ -1,2 +1,35 @@ -from .server import ServerInterceptor # noqa: F401 -from .client import ClientInterceptor # noqa: F401 +from grpc import Channel + +from sentry_sdk.integrations import Integration +from .client import ClientInterceptor + + +def patch_grpc_channels() -> None: + """Monkeypatch grpc.secure_channel and grpc.insecure_channel + with functions intercepting the returned channels. + """ + import grpc + + old_insecure_channel = grpc.insecure_channel + + def sentry_patched_insecure_channel(*args, **kwargs) -> Channel: + channel = old_insecure_channel(*args, **kwargs) + return grpc.intercept_channel(channel, ClientInterceptor()) + + grpc.insecure_channel = sentry_patched_insecure_channel + + old_secure_channel = grpc.secure_channel + + def sentry_patched_secure_channel(*args, **kwargs) -> Channel: + channel = old_secure_channel(*args, **kwargs) + return grpc.intercept_channel(channel, ClientInterceptor()) + + grpc.secure_channel = sentry_patched_secure_channel + + +class GRPCIntegration(Integration): + identifier = "grpc" + + @staticmethod + def setup_once() -> None: + patch_grpc_channels() diff --git a/tests/integrations/grpc/test_grpc.py b/tests/integrations/grpc/test_grpc.py index 92883e9256..604aca4526 100644 --- a/tests/integrations/grpc/test_grpc.py +++ b/tests/integrations/grpc/test_grpc.py @@ -9,7 +9,7 @@ from sentry_sdk import Hub, start_transaction from sentry_sdk.consts import OP -from sentry_sdk.integrations.grpc.client import ClientInterceptor +from sentry_sdk.integrations.grpc import GRPCIntegration from sentry_sdk.integrations.grpc.server import ServerInterceptor from tests.integrations.grpc.grpc_test_service_pb2 import gRPCTestMessage from tests.integrations.grpc.grpc_test_service_pb2_grpc import ( @@ -94,14 +94,12 @@ def test_grpc_server_continues_transaction(sentry_init, capture_events_forksafe) @pytest.mark.forked def test_grpc_client_starts_span(sentry_init, capture_events_forksafe): - sentry_init(traces_sample_rate=1.0) + sentry_init(traces_sample_rate=1.0, integrations=[GRPCIntegration()]) events = capture_events_forksafe() - interceptors = [ClientInterceptor()] server = _set_up() with grpc.insecure_channel(f"localhost:{PORT}") as channel: - channel = grpc.intercept_channel(channel, *interceptors) stub = gRPCTestServiceStub(channel) with start_transaction(): @@ -131,14 +129,12 @@ def test_grpc_client_starts_span(sentry_init, capture_events_forksafe): def test_grpc_client_and_servers_interceptors_integration( sentry_init, capture_events_forksafe ): - sentry_init(traces_sample_rate=1.0) + sentry_init(traces_sample_rate=1.0, integrations=[GRPCIntegration()]) events = capture_events_forksafe() - interceptors = [ClientInterceptor()] server = _set_up() with grpc.insecure_channel(f"localhost:{PORT}") as channel: - channel = grpc.intercept_channel(channel, *interceptors) stub = gRPCTestServiceStub(channel) with start_transaction(): From dfb87261d74d170b9296c1cdbab4f7ff459b0bab Mon Sep 17 00:00:00 2001 From: Florian Dellekart Date: Sat, 26 Aug 2023 15:47:51 +0200 Subject: [PATCH 04/45] feat: Add patch with gRPC server side interceptors for grpc.server --- sentry_sdk/integrations/grpc/__init__.py | 26 +++++++++++++++++++++++- tests/integrations/grpc/test_grpc.py | 6 ++---- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/sentry_sdk/integrations/grpc/__init__.py b/sentry_sdk/integrations/grpc/__init__.py index 21b3cbbb6e..1d407faa2a 100644 --- a/sentry_sdk/integrations/grpc/__init__.py +++ b/sentry_sdk/integrations/grpc/__init__.py @@ -1,7 +1,10 @@ -from grpc import Channel +from typing import List, Optional + +from grpc import Channel, Server from sentry_sdk.integrations import Integration from .client import ClientInterceptor +from .server import ServerInterceptor def patch_grpc_channels() -> None: @@ -27,9 +30,30 @@ def sentry_patched_secure_channel(*args, **kwargs) -> Channel: grpc.secure_channel = sentry_patched_secure_channel +def patch_grpc_server() -> None: + """Monkeypatch grpc.server to add server-side interceptor.""" + import grpc + + old_server = grpc.server + + def sentry_patched_server( + *args, interceptors: Optional[List[grpc.ServerInterceptor]] = None, **kwargs + ) -> Server: + server_interceptor = ServerInterceptor() + if interceptors is None: + interceptors = [server_interceptor] + else: + interceptors.append(server_interceptor) + + return old_server(*args, interceptors=interceptors, **kwargs) + + grpc.server = sentry_patched_server + + class GRPCIntegration(Integration): identifier = "grpc" @staticmethod def setup_once() -> None: patch_grpc_channels() + patch_grpc_server() diff --git a/tests/integrations/grpc/test_grpc.py b/tests/integrations/grpc/test_grpc.py index 604aca4526..2f1ff9eef8 100644 --- a/tests/integrations/grpc/test_grpc.py +++ b/tests/integrations/grpc/test_grpc.py @@ -10,7 +10,6 @@ from sentry_sdk import Hub, start_transaction from sentry_sdk.consts import OP from sentry_sdk.integrations.grpc import GRPCIntegration -from sentry_sdk.integrations.grpc.server import ServerInterceptor from tests.integrations.grpc.grpc_test_service_pb2 import gRPCTestMessage from tests.integrations.grpc.grpc_test_service_pb2_grpc import ( gRPCTestServiceServicer, @@ -24,7 +23,7 @@ @pytest.mark.forked def test_grpc_server_starts_transaction(sentry_init, capture_events_forksafe): - sentry_init(traces_sample_rate=1.0) + sentry_init(traces_sample_rate=1.0, integrations=[GRPCIntegration()]) events = capture_events_forksafe() server = _set_up() @@ -49,7 +48,7 @@ def test_grpc_server_starts_transaction(sentry_init, capture_events_forksafe): @pytest.mark.forked def test_grpc_server_continues_transaction(sentry_init, capture_events_forksafe): - sentry_init(traces_sample_rate=1.0) + sentry_init(traces_sample_rate=1.0, integrations=[GRPCIntegration()]) events = capture_events_forksafe() server = _set_up() @@ -155,7 +154,6 @@ def test_grpc_client_and_servers_interceptors_integration( def _set_up(): server = grpc.server( futures.ThreadPoolExecutor(max_workers=2), - interceptors=[ServerInterceptor(find_name=_find_name)], ) add_gRPCTestServiceServicer_to_server(TestService, server) From aaf705eacde0d136d5e69547a8cf292e94b43555 Mon Sep 17 00:00:00 2001 From: Florian Dellekart Date: Sun, 27 Aug 2023 15:47:18 +0200 Subject: [PATCH 05/45] test: Add tests to verify gRPC integration does not break other interceptors --- tests/integrations/grpc/test_grpc.py | 86 +++++++++++++++++++++++++++- 1 file changed, 84 insertions(+), 2 deletions(-) diff --git a/tests/integrations/grpc/test_grpc.py b/tests/integrations/grpc/test_grpc.py index 2f1ff9eef8..8208a36a62 100644 --- a/tests/integrations/grpc/test_grpc.py +++ b/tests/integrations/grpc/test_grpc.py @@ -1,8 +1,9 @@ from __future__ import absolute_import import os - +from typing import List, Optional from concurrent import futures +from unittest.mock import Mock import grpc import pytest @@ -46,6 +47,39 @@ def test_grpc_server_starts_transaction(sentry_init, capture_events_forksafe): assert span["op"] == "test" +@pytest.mark.forked +def test_grpc_server_other_interceptors(sentry_init, capture_events_forksafe): + """Ensure compatibility with additional server interceptors.""" + sentry_init(traces_sample_rate=1.0, integrations=[GRPCIntegration()]) + events = capture_events_forksafe() + mock_intercept = lambda continuation, handler_call_details: continuation( + handler_call_details + ) + mock_interceptor = Mock() + mock_interceptor.intercept_service.side_effect = mock_intercept + + server = _set_up(interceptors=[mock_interceptor]) + + with grpc.insecure_channel(f"localhost:{PORT}") as channel: + stub = gRPCTestServiceStub(channel) + stub.TestServe(gRPCTestMessage(text="test")) + + _tear_down(server=server) + + mock_interceptor.intercept_service.assert_called_once() + + events.write_file.close() + event = events.read_event() + span = event["spans"][0] + + assert event["type"] == "transaction" + assert event["transaction_info"] == { + "source": "custom", + } + assert event["contexts"]["trace"]["op"] == OP.GRPC_SERVER + assert span["op"] == "test" + + @pytest.mark.forked def test_grpc_server_continues_transaction(sentry_init, capture_events_forksafe): sentry_init(traces_sample_rate=1.0, integrations=[GRPCIntegration()]) @@ -124,6 +158,53 @@ def test_grpc_client_starts_span(sentry_init, capture_events_forksafe): } +# using unittest.mock.Mock not possible because grpc verifies +# that the interceptor is of the correct type +class MockClientInterceptor(grpc.UnaryUnaryClientInterceptor): + call_counter = 0 + + def intercept_unary_unary(self, continuation, client_call_details, request): + self.__class__.call_counter += 1 + return continuation(client_call_details, request) + + +@pytest.mark.forked +def test_grpc_client_other_interceptor(sentry_init, capture_events_forksafe): + """Ensure compatibility with additional client interceptors.""" + sentry_init(traces_sample_rate=1.0, integrations=[GRPCIntegration()]) + events = capture_events_forksafe() + + server = _set_up() + + with grpc.insecure_channel(f"localhost:{PORT}") as channel: + channel = grpc.intercept_channel(channel, MockClientInterceptor()) + stub = gRPCTestServiceStub(channel) + + with start_transaction(): + stub.TestServe(gRPCTestMessage(text="test")) + + _tear_down(server=server) + + assert MockClientInterceptor.call_counter == 1 + + events.write_file.close() + events.read_event() + local_transaction = events.read_event() + span = local_transaction["spans"][0] + + assert len(local_transaction["spans"]) == 1 + assert span["op"] == OP.GRPC_CLIENT + assert ( + span["description"] + == "unary unary call to /grpc_test_server.gRPCTestService/TestServe" + ) + assert span["data"] == { + "type": "unary unary", + "method": "/grpc_test_server.gRPCTestService/TestServe", + "code": "OK", + } + + @pytest.mark.forked def test_grpc_client_and_servers_interceptors_integration( sentry_init, capture_events_forksafe @@ -151,9 +232,10 @@ def test_grpc_client_and_servers_interceptors_integration( ) -def _set_up(): +def _set_up(interceptors: Optional[List[grpc.ServerInterceptor]] = None): server = grpc.server( futures.ThreadPoolExecutor(max_workers=2), + interceptors=interceptors, ) add_gRPCTestServiceServicer_to_server(TestService, server) From 155e998deb30e6933d5a0012ca8913e1567310c0 Mon Sep 17 00:00:00 2001 From: Florian Dellekart Date: Sun, 27 Aug 2023 18:50:20 +0200 Subject: [PATCH 06/45] feat: Add monkeypatch for async server to gRPC integrations --- sentry_sdk/integrations/grpc/__init__.py | 17 +++++++++++++++++ tests/integrations/grpc/test_aio_grpc.py | 17 ++++++----------- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/sentry_sdk/integrations/grpc/__init__.py b/sentry_sdk/integrations/grpc/__init__.py index 1d407faa2a..4b65a6598f 100644 --- a/sentry_sdk/integrations/grpc/__init__.py +++ b/sentry_sdk/integrations/grpc/__init__.py @@ -5,6 +5,7 @@ from sentry_sdk.integrations import Integration from .client import ClientInterceptor from .server import ServerInterceptor +from .aio.server import ServerInterceptor as AsyncServerInterceptor def patch_grpc_channels() -> None: @@ -47,7 +48,23 @@ def sentry_patched_server( return old_server(*args, interceptors=interceptors, **kwargs) + old_aio_server = grpc.aio.server + + def sentry_patched_aio_server( + *args, interceptors: Optional[List[grpc.ServerInterceptor]] = None, **kwargs + ) -> Server: + server_interceptor = AsyncServerInterceptor( + find_name=lambda request: request.__class__ + ) + if interceptors is None: + interceptors = [server_interceptor] + else: + interceptors.append(server_interceptor) + + return old_aio_server(*args, interceptors=interceptors, **kwargs) + grpc.server = sentry_patched_server + grpc.aio.server = sentry_patched_aio_server class GRPCIntegration(Integration): diff --git a/tests/integrations/grpc/test_aio_grpc.py b/tests/integrations/grpc/test_aio_grpc.py index 0337647363..528b633d75 100644 --- a/tests/integrations/grpc/test_aio_grpc.py +++ b/tests/integrations/grpc/test_aio_grpc.py @@ -10,7 +10,7 @@ from sentry_sdk import Hub from sentry_sdk.consts import OP -from sentry_sdk.integrations.grpc.aio.server import ServerInterceptor +from sentry_sdk.integrations.grpc import GRPCIntegration from tests.integrations.grpc.grpc_test_service_pb2 import gRPCTestMessage from tests.integrations.grpc.grpc_test_service_pb2_grpc import ( gRPCTestServiceServicer, @@ -31,10 +31,9 @@ def event_loop(request): @pytest_asyncio.fixture(scope="function") -async def grpc_server(event_loop): - server = grpc.aio.server( - interceptors=[ServerInterceptor(find_name=lambda request: request.__class__)] - ) +async def grpc_server(sentry_init, event_loop): + sentry_init(traces_sample_rate=1.0, integrations=[GRPCIntegration()]) + server = grpc.aio.server() server.add_insecure_port(f"[::]:{AIO_PORT}") add_gRPCTestServiceServicer_to_server(TestService, server) @@ -47,8 +46,7 @@ async def grpc_server(event_loop): @pytest.mark.asyncio -async def test_grpc_server_starts_transaction(sentry_init, capture_events, grpc_server): - sentry_init(traces_sample_rate=1.0) +async def test_grpc_server_starts_transaction(capture_events, grpc_server): events = capture_events() async with grpc.aio.insecure_channel(f"localhost:{AIO_PORT}") as channel: @@ -67,10 +65,7 @@ async def test_grpc_server_starts_transaction(sentry_init, capture_events, grpc_ @pytest.mark.asyncio -async def test_grpc_server_continues_transaction( - sentry_init, capture_events, grpc_server -): - sentry_init(traces_sample_rate=1.0) +async def test_grpc_server_continues_transaction(capture_events, grpc_server): events = capture_events() async with grpc.aio.insecure_channel(f"localhost:{AIO_PORT}") as channel: From 0b487e18fe03715031eeff6db7695a9b579b8236 Mon Sep 17 00:00:00 2001 From: Florian Dellekart Date: Fri, 1 Sep 2023 13:46:34 +0200 Subject: [PATCH 07/45] feat(grpc): Add async unary unary client interceptor --- sentry_sdk/integrations/grpc/aio/client.py | 35 ++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 sentry_sdk/integrations/grpc/aio/client.py diff --git a/sentry_sdk/integrations/grpc/aio/client.py b/sentry_sdk/integrations/grpc/aio/client.py new file mode 100644 index 0000000000..e7e599f47c --- /dev/null +++ b/sentry_sdk/integrations/grpc/aio/client.py @@ -0,0 +1,35 @@ +from typing import Callable, Union + +from grpc.aio import UnaryUnaryClientInterceptor +from grpc.aio._call import UnaryUnaryCall +from grpc.aio._interceptor import ClientCallDetails +from google.protobuf.message import Message + +from sentry_sdk import Hub +from sentry_sdk.consts import OP + + +class AsyncClientInterceptor(UnaryUnaryClientInterceptor): + async def intercept_unary_unary( + self, + continuation: Callable[[ClientCallDetails, Message], UnaryUnaryCall], + client_call_details: ClientCallDetails, + request: Message, + ) -> Union[UnaryUnaryCall, Message]: + hub = Hub.current + method = client_call_details.method + + with hub.start_span( + op=OP.GRPC_CLIENT, description="unary unary call to %s" % method.decode() + ) as span: + span.set_data("type", "unary unary") + span.set_data("method", method) + + for key, value in hub.iter_trace_propagation_headers(): + client_call_details.metadata.add(key, value) + + response = await continuation(client_call_details, request) + status_code = await response.code() + span.set_data("code", status_code.name) + + return response From ce73a3655beebac02cc07398c55cc6dee76d229c Mon Sep 17 00:00:00 2001 From: Florian Dellekart Date: Fri, 1 Sep 2023 13:56:17 +0200 Subject: [PATCH 08/45] feat(grpc): Add monkeypatching for async channels to integration --- sentry_sdk/integrations/grpc/__init__.py | 36 +++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/grpc/__init__.py b/sentry_sdk/integrations/grpc/__init__.py index 4b65a6598f..d5795f66a2 100644 --- a/sentry_sdk/integrations/grpc/__init__.py +++ b/sentry_sdk/integrations/grpc/__init__.py @@ -1,11 +1,13 @@ -from typing import List, Optional +from typing import List, Optional, Sequence from grpc import Channel, Server +from grpc.aio import Channel as AsyncChannel from sentry_sdk.integrations import Integration from .client import ClientInterceptor from .server import ServerInterceptor from .aio.server import ServerInterceptor as AsyncServerInterceptor +from .aio.client import AsyncClientInterceptor def patch_grpc_channels() -> None: @@ -30,6 +32,38 @@ def sentry_patched_secure_channel(*args, **kwargs) -> Channel: grpc.secure_channel = sentry_patched_secure_channel + old_aio_insecure_channel = grpc.aio.insecure_channel + + def sentry_patched_insecure_aio_channel( + *args, + interceptors: Optional[Sequence[grpc.aio.ClientInterceptor]] = None, + **kwargs + ) -> AsyncChannel: + interceptor = AsyncClientInterceptor() + if interceptors is None: + interceptors = [interceptor] + else: + interceptors.append(interceptor) + return old_aio_insecure_channel(*args, interceptors=interceptors, **kwargs) + + grpc.aio.insecure_channel = sentry_patched_insecure_aio_channel + + old_aio_secure_channel = grpc.aio.secure_channel + + def sentry_patched_secure_channel( + *args, + interceptors: Optional[Sequence[grpc.aio.ClientInterceptor]] = None, + **kwargs + ) -> AsyncChannel: + interceptor = AsyncClientInterceptor() + if interceptors is None: + interceptors = [interceptor] + else: + interceptors.append(interceptor) + return old_aio_secure_channel(*args, interceptors=interceptors, **kwargs) + + grpc.aio.secure_channel = sentry_patched_secure_channel + def patch_grpc_server() -> None: """Monkeypatch grpc.server to add server-side interceptor.""" From 96c2c911af6123dab6cbe002d3aec6ca160b8934 Mon Sep 17 00:00:00 2001 From: Florian Dellekart Date: Fri, 1 Sep 2023 13:59:43 +0200 Subject: [PATCH 09/45] test(grpc): Add test for aio client integration --- tests/integrations/grpc/test_aio_grpc.py | 29 +++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/tests/integrations/grpc/test_aio_grpc.py b/tests/integrations/grpc/test_aio_grpc.py index 528b633d75..85e88b0b5a 100644 --- a/tests/integrations/grpc/test_aio_grpc.py +++ b/tests/integrations/grpc/test_aio_grpc.py @@ -8,7 +8,7 @@ import pytest_asyncio import sentry_sdk -from sentry_sdk import Hub +from sentry_sdk import Hub, start_transaction from sentry_sdk.consts import OP from sentry_sdk.integrations.grpc import GRPCIntegration from tests.integrations.grpc.grpc_test_service_pb2 import gRPCTestMessage @@ -125,6 +125,33 @@ async def test_grpc_server_exception(sentry_init, capture_events, grpc_server): assert event["exception"]["values"][0]["mechanism"]["type"] == "gRPC" +@pytest.mark.asyncio +async def test_grpc_client_starts_span(grpc_server, capture_events_forksafe): + events = capture_events_forksafe() + + async with grpc.aio.insecure_channel(f"localhost:{AIO_PORT}") as channel: + stub = gRPCTestServiceStub(channel) + with start_transaction(): + await stub.TestServe(gRPCTestMessage(text="test")) + + events.write_file.close() + events.read_event() + local_transaction = events.read_event() + span = local_transaction["spans"][0] + + assert len(local_transaction["spans"]) == 1 + assert span["op"] == OP.GRPC_CLIENT + assert ( + span["description"] + == "unary unary call to /grpc_test_server.gRPCTestService/TestServe" + ) + assert span["data"] == { + "type": "unary unary", + "method": "/grpc_test_server.gRPCTestService/TestServe", + "code": "OK", + } + + class TestService(gRPCTestServiceServicer): class TestException(Exception): def __init__(self): From 41039298847f3826fef8d75aaaa9c01b358dc836 Mon Sep 17 00:00:00 2001 From: Florian Dellekart Date: Sun, 10 Sep 2023 13:05:49 +0200 Subject: [PATCH 10/45] refactor: Avoid unnecessary code duplication in grpc integration Also previous logic was limited to interceptors being a list, however, it is typed as a general sequence in grpc package. --- sentry_sdk/integrations/grpc/__init__.py | 113 +++++++++------------ sentry_sdk/integrations/grpc/aio/client.py | 3 + 2 files changed, 49 insertions(+), 67 deletions(-) diff --git a/sentry_sdk/integrations/grpc/__init__.py b/sentry_sdk/integrations/grpc/__init__.py index d5795f66a2..809a28ddea 100644 --- a/sentry_sdk/integrations/grpc/__init__.py +++ b/sentry_sdk/integrations/grpc/__init__.py @@ -1,7 +1,10 @@ -from typing import List, Optional, Sequence +from typing import List, Optional, Sequence, Callable, ParamSpec +from functools import wraps -from grpc import Channel, Server +import grpc +from grpc import Channel, Server, intercept_channel from grpc.aio import Channel as AsyncChannel +from grpc.aio import Server as AsyncServer from sentry_sdk.integrations import Integration from .client import ClientInterceptor @@ -9,96 +12,64 @@ from .aio.server import ServerInterceptor as AsyncServerInterceptor from .aio.client import AsyncClientInterceptor +P = ParamSpec("P") -def patch_grpc_channels() -> None: - """Monkeypatch grpc.secure_channel and grpc.insecure_channel - with functions intercepting the returned channels. - """ - import grpc - old_insecure_channel = grpc.insecure_channel +def _wrap_channel_sync(func: Callable[P, Channel]) -> Callable[P, Channel]: + "Wrapper for synchronous secure and insecure channel." - def sentry_patched_insecure_channel(*args, **kwargs) -> Channel: - channel = old_insecure_channel(*args, **kwargs) - return grpc.intercept_channel(channel, ClientInterceptor()) + @wraps(func) + def patched_channel(*args, **kwargs) -> Channel: + channel = func(*args, **kwargs) + return intercept_channel(channel, ClientInterceptor()) - grpc.insecure_channel = sentry_patched_insecure_channel + return patched_channel - old_secure_channel = grpc.secure_channel - def sentry_patched_secure_channel(*args, **kwargs) -> Channel: - channel = old_secure_channel(*args, **kwargs) - return grpc.intercept_channel(channel, ClientInterceptor()) +def _wrap_channel_async(func: Callable[P, AsyncChannel]) -> Callable[P, AsyncChannel]: + "Wrapper for asynchronous secure and insecure channel." - grpc.secure_channel = sentry_patched_secure_channel - - old_aio_insecure_channel = grpc.aio.insecure_channel - - def sentry_patched_insecure_aio_channel( + @wraps(func) + def patched_channel( *args, interceptors: Optional[Sequence[grpc.aio.ClientInterceptor]] = None, **kwargs - ) -> AsyncChannel: + ) -> Channel: interceptor = AsyncClientInterceptor() - if interceptors is None: - interceptors = [interceptor] - else: - interceptors.append(interceptor) - return old_aio_insecure_channel(*args, interceptors=interceptors, **kwargs) - - grpc.aio.insecure_channel = sentry_patched_insecure_aio_channel - - old_aio_secure_channel = grpc.aio.secure_channel + interceptors = [interceptor, *(interceptors or [])] + return func(*args, interceptors=interceptors, **kwargs) - def sentry_patched_secure_channel( - *args, - interceptors: Optional[Sequence[grpc.aio.ClientInterceptor]] = None, - **kwargs - ) -> AsyncChannel: - interceptor = AsyncClientInterceptor() - if interceptors is None: - interceptors = [interceptor] - else: - interceptors.append(interceptor) - return old_aio_secure_channel(*args, interceptors=interceptors, **kwargs) + return patched_channel - grpc.aio.secure_channel = sentry_patched_secure_channel +def _wrap_sync_server(func: Callable[P, Server]) -> Callable[P, Server]: + """Wrapper for synchronous server.""" -def patch_grpc_server() -> None: - """Monkeypatch grpc.server to add server-side interceptor.""" - import grpc - - old_server = grpc.server - - def sentry_patched_server( + @wraps(func) + def patched_server( *args, interceptors: Optional[List[grpc.ServerInterceptor]] = None, **kwargs ) -> Server: server_interceptor = ServerInterceptor() - if interceptors is None: - interceptors = [server_interceptor] - else: - interceptors.append(server_interceptor) + interceptors = [server_interceptor, *(interceptors or [])] + return func(*args, interceptors=interceptors, **kwargs) - return old_server(*args, interceptors=interceptors, **kwargs) + return patched_server - old_aio_server = grpc.aio.server - def sentry_patched_aio_server( +def _wrap_async_server(func: Callable[P, AsyncServer]) -> Callable[P, AsyncServer]: + """Wrapper for asynchronous server.""" + + @wraps(func) + def patched_aio_server( *args, interceptors: Optional[List[grpc.ServerInterceptor]] = None, **kwargs ) -> Server: server_interceptor = AsyncServerInterceptor( find_name=lambda request: request.__class__ ) - if interceptors is None: - interceptors = [server_interceptor] - else: - interceptors.append(server_interceptor) - - return old_aio_server(*args, interceptors=interceptors, **kwargs) + interceptors = [server_interceptor, *(interceptors or [])] + return func(*args, interceptors=interceptors, **kwargs) - grpc.server = sentry_patched_server - grpc.aio.server = sentry_patched_aio_server + return patched_aio_server class GRPCIntegration(Integration): @@ -106,5 +77,13 @@ class GRPCIntegration(Integration): @staticmethod def setup_once() -> None: - patch_grpc_channels() - patch_grpc_server() + import grpc + + grpc.insecure_channel = _wrap_channel_sync(grpc.insecure_channel) + grpc.secure_channel = _wrap_channel_sync(grpc.secure_channel) + + grpc.aio.insecure_channel = _wrap_channel_async(grpc.aio.insecure_channel) + grpc.aio.secure_channel = _wrap_channel_async(grpc.aio.secure_channel) + + grpc.server = _wrap_sync_server(grpc.server) + grpc.aio.server = _wrap_async_server(grpc.aio.server) diff --git a/sentry_sdk/integrations/grpc/aio/client.py b/sentry_sdk/integrations/grpc/aio/client.py index e7e599f47c..17526e2af5 100644 --- a/sentry_sdk/integrations/grpc/aio/client.py +++ b/sentry_sdk/integrations/grpc/aio/client.py @@ -26,6 +26,9 @@ async def intercept_unary_unary( span.set_data("method", method) for key, value in hub.iter_trace_propagation_headers(): + # Currently broken + # Waiting for response here + # https://github.com/grpc/grpc/issues/34298 client_call_details.metadata.add(key, value) response = await continuation(client_call_details, request) From a8ddcf647bf6674ad9d476730a084d69eeb6b293 Mon Sep 17 00:00:00 2001 From: Florian Dellekart Date: Wed, 13 Sep 2023 13:59:56 +0200 Subject: [PATCH 11/45] fix: gRPC async metadata can be tuple although differently typed Opened an issue in grpc repo to ask if this is inteded behaviour If it should be changed one day the the .add method of the metadata object would avoid reconstructing the whole client call details. Link to issue: https://github.com/grpc/grpc/issues/34298 --- sentry_sdk/integrations/grpc/aio/client.py | 28 ++++++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/sentry_sdk/integrations/grpc/aio/client.py b/sentry_sdk/integrations/grpc/aio/client.py index 17526e2af5..05fc43ed51 100644 --- a/sentry_sdk/integrations/grpc/aio/client.py +++ b/sentry_sdk/integrations/grpc/aio/client.py @@ -25,14 +25,32 @@ async def intercept_unary_unary( span.set_data("type", "unary unary") span.set_data("method", method) - for key, value in hub.iter_trace_propagation_headers(): - # Currently broken - # Waiting for response here - # https://github.com/grpc/grpc/issues/34298 - client_call_details.metadata.add(key, value) + client_call_details = self._update_client_call_details_metadata_from_hub( + client_call_details, hub + ) response = await continuation(client_call_details, request) status_code = await response.code() span.set_data("code", status_code.name) return response + + @staticmethod + def _update_client_call_details_metadata_from_hub( + client_call_details: ClientCallDetails, hub: Hub + ): + metadata = ( + list(client_call_details.metadata) if client_call_details.metadata else [] + ) + for key, value in hub.iter_trace_propagation_headers(): + metadata.append((key, value)) + + client_call_details = ClientCallDetails( + method=client_call_details.method, + timeout=client_call_details.timeout, + metadata=metadata, + credentials=client_call_details.credentials, + wait_for_ready=client_call_details.wait_for_ready, + ) + + return client_call_details From 9f1bc889328ec9a87f03d0c73a78c8cf5db4ce26 Mon Sep 17 00:00:00 2001 From: Florian Dellekart Date: Fri, 22 Sep 2023 14:34:16 +0200 Subject: [PATCH 12/45] refactor(grpc): consistent naming and imports --- sentry_sdk/integrations/grpc/__init__.py | 2 +- sentry_sdk/integrations/grpc/aio/__init__.py | 1 + sentry_sdk/integrations/grpc/aio/client.py | 6 ++---- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/sentry_sdk/integrations/grpc/__init__.py b/sentry_sdk/integrations/grpc/__init__.py index 809a28ddea..6223fce044 100644 --- a/sentry_sdk/integrations/grpc/__init__.py +++ b/sentry_sdk/integrations/grpc/__init__.py @@ -10,7 +10,7 @@ from .client import ClientInterceptor from .server import ServerInterceptor from .aio.server import ServerInterceptor as AsyncServerInterceptor -from .aio.client import AsyncClientInterceptor +from .aio.client import ClientInterceptor as AsyncClientInterceptor P = ParamSpec("P") diff --git a/sentry_sdk/integrations/grpc/aio/__init__.py b/sentry_sdk/integrations/grpc/aio/__init__.py index c3695e948e..59bfd502e5 100644 --- a/sentry_sdk/integrations/grpc/aio/__init__.py +++ b/sentry_sdk/integrations/grpc/aio/__init__.py @@ -1 +1,2 @@ from .server import ServerInterceptor # noqa: F401 +from .client import ClientInterceptor # noqa: F401 diff --git a/sentry_sdk/integrations/grpc/aio/client.py b/sentry_sdk/integrations/grpc/aio/client.py index 05fc43ed51..908ccebcf0 100644 --- a/sentry_sdk/integrations/grpc/aio/client.py +++ b/sentry_sdk/integrations/grpc/aio/client.py @@ -1,15 +1,13 @@ from typing import Callable, Union -from grpc.aio import UnaryUnaryClientInterceptor -from grpc.aio._call import UnaryUnaryCall -from grpc.aio._interceptor import ClientCallDetails +from grpc.aio import UnaryUnaryClientInterceptor, ClientCallDetails, UnaryUnaryCall from google.protobuf.message import Message from sentry_sdk import Hub from sentry_sdk.consts import OP -class AsyncClientInterceptor(UnaryUnaryClientInterceptor): +class ClientInterceptor(UnaryUnaryClientInterceptor): async def intercept_unary_unary( self, continuation: Callable[[ClientCallDetails, Message], UnaryUnaryCall], From b43784fa6c8a4cbab554963e51fbd376fa9e63b7 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Wed, 27 Sep 2023 14:11:41 +0200 Subject: [PATCH 13/45] Added pytest-asyncio to test deps --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 83b43ad4c6..4761a01b76 100644 --- a/tox.ini +++ b/tox.ini @@ -333,6 +333,7 @@ deps = grpc: protobuf grpc: mypy-protobuf grpc: types-protobuf + grpc: pytest-asyncio # HTTPX httpx: pytest-httpx From 34bad4c399d593653592b5dd93cfa2c5f32470b8 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Wed, 27 Sep 2023 14:23:30 +0200 Subject: [PATCH 14/45] Added types to linter requirements --- linter-requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/linter-requirements.txt b/linter-requirements.txt index d1108f8eae..289df0cd7f 100644 --- a/linter-requirements.txt +++ b/linter-requirements.txt @@ -2,6 +2,7 @@ mypy black flake8==5.0.4 # flake8 depends on pyflakes>=3.0.0 and this dropped support for Python 2 "# type:" comments types-certifi +types-protobuf types-redis types-setuptools pymongo # There is no separate types module. From 5a89d9b5de9ac5894d588cf1a2dc7075b2d75ee6 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Wed, 27 Sep 2023 15:00:58 +0200 Subject: [PATCH 15/45] Made mechanism type lowercase --- sentry_sdk/integrations/grpc/aio/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/grpc/aio/server.py b/sentry_sdk/integrations/grpc/aio/server.py index 34554d866f..ded1e16eba 100644 --- a/sentry_sdk/integrations/grpc/aio/server.py +++ b/sentry_sdk/integrations/grpc/aio/server.py @@ -57,7 +57,7 @@ async def wrapped(request, context): except Exception as exc: event, hint = event_from_exception( exc, - mechanism={"type": "gRPC", "handled": False}, + mechanism={"type": "grpc", "handled": False}, ) hub.capture_event(event, hint=hint) raise From 8341a0d399073e6931441373ee76402ddc031390 Mon Sep 17 00:00:00 2001 From: Florian Dellekart Date: Fri, 29 Sep 2023 18:26:57 +0200 Subject: [PATCH 16/45] fix: typing of async gRPC integration --- linter-requirements.txt | 1 + scripts/build_aws_lambda_layer.py | 3 ++- sentry_sdk/integrations/grpc/__init__.py | 28 ++++++++++++---------- sentry_sdk/integrations/grpc/aio/client.py | 4 ++-- sentry_sdk/integrations/grpc/client.py | 2 +- sentry_sdk/integrations/grpc/server.py | 4 ++-- tox.ini | 1 + 7 files changed, 25 insertions(+), 18 deletions(-) diff --git a/linter-requirements.txt b/linter-requirements.txt index d1108f8eae..b124ea6fc4 100644 --- a/linter-requirements.txt +++ b/linter-requirements.txt @@ -9,3 +9,4 @@ loguru # There is no separate types module. flake8-bugbear pep8-naming pre-commit # local linting +types-protobuf diff --git a/scripts/build_aws_lambda_layer.py b/scripts/build_aws_lambda_layer.py index 829b7e31d9..d551097649 100644 --- a/scripts/build_aws_lambda_layer.py +++ b/scripts/build_aws_lambda_layer.py @@ -76,9 +76,10 @@ def zip(self): shutil.copy( os.path.join(self.base_dir, self.out_zip_filename), - os.path.abspath(DIST_PATH) + os.path.abspath(DIST_PATH), ) + def build_packaged_zip(): with tempfile.TemporaryDirectory() as base_dir: layer_builder = LayerBuilder(base_dir) diff --git a/sentry_sdk/integrations/grpc/__init__.py b/sentry_sdk/integrations/grpc/__init__.py index 6223fce044..249ce81d5e 100644 --- a/sentry_sdk/integrations/grpc/__init__.py +++ b/sentry_sdk/integrations/grpc/__init__.py @@ -1,4 +1,4 @@ -from typing import List, Optional, Sequence, Callable, ParamSpec +from typing import List, Optional, Sequence, Callable, ParamSpec, Any from functools import wraps import grpc @@ -19,7 +19,7 @@ def _wrap_channel_sync(func: Callable[P, Channel]) -> Callable[P, Channel]: "Wrapper for synchronous secure and insecure channel." @wraps(func) - def patched_channel(*args, **kwargs) -> Channel: + def patched_channel(*args: Any, **kwargs: Any) -> Channel: channel = func(*args, **kwargs) return intercept_channel(channel, ClientInterceptor()) @@ -31,15 +31,15 @@ def _wrap_channel_async(func: Callable[P, AsyncChannel]) -> Callable[P, AsyncCha @wraps(func) def patched_channel( - *args, + *args: P.args, interceptors: Optional[Sequence[grpc.aio.ClientInterceptor]] = None, - **kwargs + **kwargs: P.kwargs, ) -> Channel: interceptor = AsyncClientInterceptor() interceptors = [interceptor, *(interceptors or [])] - return func(*args, interceptors=interceptors, **kwargs) + return func(*args, interceptors=interceptors, **kwargs) # type: ignore - return patched_channel + return patched_channel # type: ignore def _wrap_sync_server(func: Callable[P, Server]) -> Callable[P, Server]: @@ -47,13 +47,15 @@ def _wrap_sync_server(func: Callable[P, Server]) -> Callable[P, Server]: @wraps(func) def patched_server( - *args, interceptors: Optional[List[grpc.ServerInterceptor]] = None, **kwargs + *args: P.args, + interceptors: Optional[List[grpc.ServerInterceptor]] = None, + **kwargs: P.kwargs, ) -> Server: server_interceptor = ServerInterceptor() interceptors = [server_interceptor, *(interceptors or [])] - return func(*args, interceptors=interceptors, **kwargs) + return func(*args, interceptors=interceptors, **kwargs) # type: ignore - return patched_server + return patched_server # type: ignore def _wrap_async_server(func: Callable[P, AsyncServer]) -> Callable[P, AsyncServer]: @@ -61,15 +63,17 @@ def _wrap_async_server(func: Callable[P, AsyncServer]) -> Callable[P, AsyncServe @wraps(func) def patched_aio_server( - *args, interceptors: Optional[List[grpc.ServerInterceptor]] = None, **kwargs + *args: P.args, + interceptors: Optional[List[grpc.ServerInterceptor]] = None, + **kwargs: P.kwargs, ) -> Server: server_interceptor = AsyncServerInterceptor( find_name=lambda request: request.__class__ ) interceptors = [server_interceptor, *(interceptors or [])] - return func(*args, interceptors=interceptors, **kwargs) + return func(*args, interceptors=interceptors, **kwargs) # type: ignore - return patched_aio_server + return patched_aio_server # type: ignore class GRPCIntegration(Integration): diff --git a/sentry_sdk/integrations/grpc/aio/client.py b/sentry_sdk/integrations/grpc/aio/client.py index 908ccebcf0..d909c50d7c 100644 --- a/sentry_sdk/integrations/grpc/aio/client.py +++ b/sentry_sdk/integrations/grpc/aio/client.py @@ -7,7 +7,7 @@ from sentry_sdk.consts import OP -class ClientInterceptor(UnaryUnaryClientInterceptor): +class ClientInterceptor(UnaryUnaryClientInterceptor): # type: ignore async def intercept_unary_unary( self, continuation: Callable[[ClientCallDetails, Message], UnaryUnaryCall], @@ -36,7 +36,7 @@ async def intercept_unary_unary( @staticmethod def _update_client_call_details_metadata_from_hub( client_call_details: ClientCallDetails, hub: Hub - ): + ) -> ClientCallDetails: metadata = ( list(client_call_details.metadata) if client_call_details.metadata else [] ) diff --git a/sentry_sdk/integrations/grpc/client.py b/sentry_sdk/integrations/grpc/client.py index 1eb3621b0b..81da724298 100644 --- a/sentry_sdk/integrations/grpc/client.py +++ b/sentry_sdk/integrations/grpc/client.py @@ -11,7 +11,7 @@ from grpc import ClientCallDetails, Call from grpc._interceptor import _UnaryOutcome from grpc.aio._interceptor import UnaryStreamCall - from google.protobuf.message import Message # type: ignore + from google.protobuf.message import Message except ImportError: raise DidNotEnable("grpcio is not installed") diff --git a/sentry_sdk/integrations/grpc/server.py b/sentry_sdk/integrations/grpc/server.py index cdeea4a2fa..240e45f8ee 100644 --- a/sentry_sdk/integrations/grpc/server.py +++ b/sentry_sdk/integrations/grpc/server.py @@ -6,7 +6,7 @@ if MYPY: from typing import Callable, Optional - from google.protobuf.message import Message # type: ignore + from google.protobuf.message import Message try: import grpc @@ -16,7 +16,7 @@ class ServerInterceptor(grpc.ServerInterceptor): # type: ignore - def __init__(self, find_name=None): + def __init__(self, find_name=None) -> None: # type: (ServerInterceptor, Optional[Callable[[ServicerContext], str]]) -> None self._find_method_name = find_name or ServerInterceptor._find_name diff --git a/tox.ini b/tox.ini index 9e1c7a664f..56f97c1e13 100644 --- a/tox.ini +++ b/tox.ini @@ -327,6 +327,7 @@ deps = grpc: protobuf grpc: mypy-protobuf grpc: types-protobuf + grpc: pytest-asyncio # HTTPX httpx: pytest-httpx From 69704d42e54d01d9e6ca7b5d79d7cd6bbcfbfd2f Mon Sep 17 00:00:00 2001 From: Florian Dellekart Date: Fri, 29 Sep 2023 18:39:05 +0200 Subject: [PATCH 17/45] feat(gRPC): Add async unary-stream interceptor --- sentry_sdk/integrations/grpc/__init__.py | 14 +++- sentry_sdk/integrations/grpc/aio/__init__.py | 2 +- sentry_sdk/integrations/grpc/aio/client.py | 77 +++++++++++++++----- 3 files changed, 69 insertions(+), 24 deletions(-) diff --git a/sentry_sdk/integrations/grpc/__init__.py b/sentry_sdk/integrations/grpc/__init__.py index 249ce81d5e..1dd2ed5da0 100644 --- a/sentry_sdk/integrations/grpc/__init__.py +++ b/sentry_sdk/integrations/grpc/__init__.py @@ -10,7 +10,12 @@ from .client import ClientInterceptor from .server import ServerInterceptor from .aio.server import ServerInterceptor as AsyncServerInterceptor -from .aio.client import ClientInterceptor as AsyncClientInterceptor +from .aio.client import ( + SentryUnaryUnaryClientInterceptor as AsyncUnaryUnaryClientInterceptor, +) +from .aio.client import ( + SentryUnaryStreamClientInterceptor as AsyncUnaryStreamClientIntercetor, +) P = ParamSpec("P") @@ -35,8 +40,11 @@ def patched_channel( interceptors: Optional[Sequence[grpc.aio.ClientInterceptor]] = None, **kwargs: P.kwargs, ) -> Channel: - interceptor = AsyncClientInterceptor() - interceptors = [interceptor, *(interceptors or [])] + sentry_interceptors = [ + AsyncUnaryUnaryClientInterceptor(), + AsyncUnaryStreamClientIntercetor(), + ] + interceptors = [*sentry_interceptors, *(interceptors or [])] return func(*args, interceptors=interceptors, **kwargs) # type: ignore return patched_channel # type: ignore diff --git a/sentry_sdk/integrations/grpc/aio/__init__.py b/sentry_sdk/integrations/grpc/aio/__init__.py index 59bfd502e5..0687681166 100644 --- a/sentry_sdk/integrations/grpc/aio/__init__.py +++ b/sentry_sdk/integrations/grpc/aio/__init__.py @@ -1,2 +1,2 @@ from .server import ServerInterceptor # noqa: F401 -from .client import ClientInterceptor # noqa: F401 +from .client import SentryClientInterceptor # noqa: F401 diff --git a/sentry_sdk/integrations/grpc/aio/client.py b/sentry_sdk/integrations/grpc/aio/client.py index d909c50d7c..9c7f13db8a 100644 --- a/sentry_sdk/integrations/grpc/aio/client.py +++ b/sentry_sdk/integrations/grpc/aio/client.py @@ -1,13 +1,41 @@ -from typing import Callable, Union +from typing import Callable, Union, AsyncIterable, Any -from grpc.aio import UnaryUnaryClientInterceptor, ClientCallDetails, UnaryUnaryCall +from grpc.aio import ( + UnaryUnaryClientInterceptor, + UnaryStreamClientInterceptor, + ClientCallDetails, + UnaryUnaryCall, + UnaryStreamCall, +) from google.protobuf.message import Message from sentry_sdk import Hub from sentry_sdk.consts import OP -class ClientInterceptor(UnaryUnaryClientInterceptor): # type: ignore +class SentryClientInterceptor: + @staticmethod + def _update_client_call_details_metadata_from_hub( + client_call_details: ClientCallDetails, hub: Hub + ) -> ClientCallDetails: + metadata = ( + list(client_call_details.metadata) if client_call_details.metadata else [] + ) + for key, value in hub.iter_trace_propagation_headers(): + metadata.append((key, value)) + + client_call_details = ClientCallDetails( + method=client_call_details.method, + timeout=client_call_details.timeout, + metadata=metadata, + credentials=client_call_details.credentials, + wait_for_ready=client_call_details.wait_for_ready, + ) + + return client_call_details + + +class SentryUnaryUnaryClientInterceptor(SentryClientInterceptor, UnaryUnaryClientInterceptor): # type: ignore async def intercept_unary_unary( self, continuation: Callable[[ClientCallDetails, Message], UnaryUnaryCall], @@ -33,22 +61,31 @@ async def intercept_unary_unary( return response - @staticmethod - def _update_client_call_details_metadata_from_hub( - client_call_details: ClientCallDetails, hub: Hub - ) -> ClientCallDetails: - metadata = ( - list(client_call_details.metadata) if client_call_details.metadata else [] - ) - for key, value in hub.iter_trace_propagation_headers(): - metadata.append((key, value)) - client_call_details = ClientCallDetails( - method=client_call_details.method, - timeout=client_call_details.timeout, - metadata=metadata, - credentials=client_call_details.credentials, - wait_for_ready=client_call_details.wait_for_ready, - ) +class SentryUnaryStreamClientInterceptor( + SentryClientInterceptor, UnaryStreamClientInterceptor # type: ignore +): + async def intercept_unary_stream( + self, + continuation: Callable[[ClientCallDetails, Message], UnaryStreamCall], + client_call_details: ClientCallDetails, + request: Message, + ) -> AsyncIterable[Any] | UnaryStreamCall: + hub = Hub.current + method = client_call_details.method - return client_call_details + with hub.start_span( + op=OP.GRPC_CLIENT, description="unary stream call %s" % method + ) as span: + span.set_data("type", "unary stream") + span.set_data("methdo", method) + + client_call_details = self._update_client_call_details_metadata_from_hub( + client_call_details, hub + ) + + response = await continuation(client_call_details, request) + status_code = await response.code() + span.set_data("code", status_code) + + return response From ccacf0a208567df5e00077a3eb421dccc6073f59 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Mon, 2 Oct 2023 12:00:55 +0200 Subject: [PATCH 18/45] Trying to make typing work in older Python versions --- sentry_sdk/integrations/grpc/__init__.py | 30 +++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/sentry_sdk/integrations/grpc/__init__.py b/sentry_sdk/integrations/grpc/__init__.py index 6223fce044..2cf153d4eb 100644 --- a/sentry_sdk/integrations/grpc/__init__.py +++ b/sentry_sdk/integrations/grpc/__init__.py @@ -1,4 +1,3 @@ -from typing import List, Optional, Sequence, Callable, ParamSpec from functools import wraps import grpc @@ -7,11 +6,36 @@ from grpc.aio import Server as AsyncServer from sentry_sdk.integrations import Integration +from sentry_sdk._types import TYPE_CHECKING + from .client import ClientInterceptor from .server import ServerInterceptor from .aio.server import ServerInterceptor as AsyncServerInterceptor from .aio.client import ClientInterceptor as AsyncClientInterceptor +if TYPE_CHECKING: + from typing import Optional, Sequence + +# Hack to get new Python features working in older versions +# without introducing a hard dependency on `typing_extensions` +# from: https://stackoverflow.com/a/71944042/300572 +if TYPE_CHECKING: + from typing import ParamSpec, Callable +else: + # Fake ParamSpec + class ParamSpec: + def __init__(self, _): + self.args = None + self.kwargs = None + + # Callable[anything] will return None + class _Callable: + def __getitem__(self, _): + return None + + # Make instances + Callable = _Callable() + P = ParamSpec("P") @@ -47,7 +71,7 @@ def _wrap_sync_server(func: Callable[P, Server]) -> Callable[P, Server]: @wraps(func) def patched_server( - *args, interceptors: Optional[List[grpc.ServerInterceptor]] = None, **kwargs + *args, interceptors: Optional[Sequence[grpc.ServerInterceptor]] = None, **kwargs ) -> Server: server_interceptor = ServerInterceptor() interceptors = [server_interceptor, *(interceptors or [])] @@ -61,7 +85,7 @@ def _wrap_async_server(func: Callable[P, AsyncServer]) -> Callable[P, AsyncServe @wraps(func) def patched_aio_server( - *args, interceptors: Optional[List[grpc.ServerInterceptor]] = None, **kwargs + *args, interceptors: Optional[Sequence[grpc.ServerInterceptor]] = None, **kwargs ) -> Server: server_interceptor = AsyncServerInterceptor( find_name=lambda request: request.__class__ From c1c0be72314d97f7d78d1609003ebfd6ee5b1fbd Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Mon, 2 Oct 2023 12:49:06 +0200 Subject: [PATCH 19/45] The tests need the types. --- sentry_sdk/integrations/grpc/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sentry_sdk/integrations/grpc/__init__.py b/sentry_sdk/integrations/grpc/__init__.py index 2cf153d4eb..125636c92f 100644 --- a/sentry_sdk/integrations/grpc/__init__.py +++ b/sentry_sdk/integrations/grpc/__init__.py @@ -13,8 +13,7 @@ from .aio.server import ServerInterceptor as AsyncServerInterceptor from .aio.client import ClientInterceptor as AsyncClientInterceptor -if TYPE_CHECKING: - from typing import Optional, Sequence +from typing import Optional, Sequence # Hack to get new Python features working in older versions # without introducing a hard dependency on `typing_extensions` From 4c71549ffc3c644573dbbcccb376e2e0812c7adb Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Mon, 2 Oct 2023 15:46:52 +0200 Subject: [PATCH 20/45] Fixed typo --- tests/integrations/grpc/test_aio_grpc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integrations/grpc/test_aio_grpc.py b/tests/integrations/grpc/test_aio_grpc.py index 85e88b0b5a..ffe84e40c4 100644 --- a/tests/integrations/grpc/test_aio_grpc.py +++ b/tests/integrations/grpc/test_aio_grpc.py @@ -122,7 +122,7 @@ async def test_grpc_server_exception(sentry_init, capture_events, grpc_server): assert event["exception"]["values"][0]["type"] == "TestService.TestException" assert event["exception"]["values"][0]["value"] == "test" assert event["exception"]["values"][0]["mechanism"]["handled"] is False - assert event["exception"]["values"][0]["mechanism"]["type"] == "gRPC" + assert event["exception"]["values"][0]["mechanism"]["type"] == "grpc" @pytest.mark.asyncio From d2e1bbebd39af9cd8fdb99799610b34f530fe59e Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Tue, 3 Oct 2023 13:30:08 +0200 Subject: [PATCH 21/45] Cleaned up tests --- .../grpc/compile_test_services.sh | 9 +++ .../grpc/grpc_aio_test_service_pb2.py | 31 ++++++++ .../grpc/grpc_aio_test_service_pb2.pyi | 11 +++ .../grpc/grpc_aio_test_service_pb2_grpc.py | 79 +++++++++++++++++++ .../grpc/grpc_test_service_pb2.py | 16 ++-- .../grpc/grpc_test_service_pb2.pyi | 39 +++------ .../grpc/protos/grpc_aio_test_service.proto | 11 +++ .../grpc/{ => protos}/grpc_test_service.proto | 0 .../{test_aio_grpc.py => test_grpc_aio.py} | 36 ++++----- 9 files changed, 177 insertions(+), 55 deletions(-) create mode 100755 tests/integrations/grpc/compile_test_services.sh create mode 100644 tests/integrations/grpc/grpc_aio_test_service_pb2.py create mode 100644 tests/integrations/grpc/grpc_aio_test_service_pb2.pyi create mode 100644 tests/integrations/grpc/grpc_aio_test_service_pb2_grpc.py create mode 100644 tests/integrations/grpc/protos/grpc_aio_test_service.proto rename tests/integrations/grpc/{ => protos}/grpc_test_service.proto (100%) rename tests/integrations/grpc/{test_aio_grpc.py => test_grpc_aio.py} (80%) diff --git a/tests/integrations/grpc/compile_test_services.sh b/tests/integrations/grpc/compile_test_services.sh new file mode 100755 index 0000000000..d244008048 --- /dev/null +++ b/tests/integrations/grpc/compile_test_services.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +# Create python files +python -m grpc_tools.protoc --proto_path=./protos --python_out=. --pyi_out=. --grpc_python_out=. ./protos/grpc_test_service.proto +python -m grpc_tools.protoc --proto_path=./protos --python_out=. --pyi_out=. --grpc_python_out=. ./protos/grpc_aio_test_service.proto + +# Fix imports in generated files +sed -i '' 's/import grpc_/import tests\.integrations\.grpc\.grpc_/g' ./grpc_test_service_pb2_grpc.py +sed -i '' 's/import grpc_/import tests\.integrations\.grpc\.grpc_/g' ./grpc_aio_test_service_pb2_grpc.py diff --git a/tests/integrations/grpc/grpc_aio_test_service_pb2.py b/tests/integrations/grpc/grpc_aio_test_service_pb2.py new file mode 100644 index 0000000000..338c5ec5b8 --- /dev/null +++ b/tests/integrations/grpc/grpc_aio_test_service_pb2.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: grpc_aio_test_service.proto +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder + +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( + b'\n\x1bgrpc_aio_test_service.proto\x12\x14grpc_aio_test_server""\n\x12gRPCaioTestMessage\x12\x0c\n\x04text\x18\x01 \x01(\t2u\n\x12gRPCaioTestService\x12_\n\tTestServe\x12(.grpc_aio_test_server.gRPCaioTestMessage\x1a(.grpc_aio_test_server.gRPCaioTestMessageb\x06proto3' +) + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages( + DESCRIPTOR, "grpc_aio_test_service_pb2", _globals +) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + _globals["_GRPCAIOTESTMESSAGE"]._serialized_start = 53 + _globals["_GRPCAIOTESTMESSAGE"]._serialized_end = 87 + _globals["_GRPCAIOTESTSERVICE"]._serialized_start = 89 + _globals["_GRPCAIOTESTSERVICE"]._serialized_end = 206 +# @@protoc_insertion_point(module_scope) diff --git a/tests/integrations/grpc/grpc_aio_test_service_pb2.pyi b/tests/integrations/grpc/grpc_aio_test_service_pb2.pyi new file mode 100644 index 0000000000..13c717d82e --- /dev/null +++ b/tests/integrations/grpc/grpc_aio_test_service_pb2.pyi @@ -0,0 +1,11 @@ +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from typing import ClassVar as _ClassVar, Optional as _Optional + +DESCRIPTOR: _descriptor.FileDescriptor + +class gRPCaioTestMessage(_message.Message): + __slots__ = ["text"] + TEXT_FIELD_NUMBER: _ClassVar[int] + text: str + def __init__(self, text: _Optional[str] = ...) -> None: ... diff --git a/tests/integrations/grpc/grpc_aio_test_service_pb2_grpc.py b/tests/integrations/grpc/grpc_aio_test_service_pb2_grpc.py new file mode 100644 index 0000000000..61e7d70b6e --- /dev/null +++ b/tests/integrations/grpc/grpc_aio_test_service_pb2_grpc.py @@ -0,0 +1,79 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + +import tests.integrations.grpc.grpc_aio_test_service_pb2 as grpc__aio__test__service__pb2 + + +class gRPCaioTestServiceStub(object): + """Missing associated documentation comment in .proto file.""" + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.TestServe = channel.unary_unary( + "/grpc_aio_test_server.gRPCaioTestService/TestServe", + request_serializer=grpc__aio__test__service__pb2.gRPCaioTestMessage.SerializeToString, + response_deserializer=grpc__aio__test__service__pb2.gRPCaioTestMessage.FromString, + ) + + +class gRPCaioTestServiceServicer(object): + """Missing associated documentation comment in .proto file.""" + + def TestServe(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details("Method not implemented!") + raise NotImplementedError("Method not implemented!") + + +def add_gRPCaioTestServiceServicer_to_server(servicer, server): + rpc_method_handlers = { + "TestServe": grpc.unary_unary_rpc_method_handler( + servicer.TestServe, + request_deserializer=grpc__aio__test__service__pb2.gRPCaioTestMessage.FromString, + response_serializer=grpc__aio__test__service__pb2.gRPCaioTestMessage.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + "grpc_aio_test_server.gRPCaioTestService", rpc_method_handlers + ) + server.add_generic_rpc_handlers((generic_handler,)) + + +# This class is part of an EXPERIMENTAL API. +class gRPCaioTestService(object): + """Missing associated documentation comment in .proto file.""" + + @staticmethod + def TestServe( + request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None, + ): + return grpc.experimental.unary_unary( + request, + target, + "/grpc_aio_test_server.gRPCaioTestService/TestServe", + grpc__aio__test__service__pb2.gRPCaioTestMessage.SerializeToString, + grpc__aio__test__service__pb2.gRPCaioTestMessage.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + ) diff --git a/tests/integrations/grpc/grpc_test_service_pb2.py b/tests/integrations/grpc/grpc_test_service_pb2.py index 94765dae2c..718fbc0081 100644 --- a/tests/integrations/grpc/grpc_test_service_pb2.py +++ b/tests/integrations/grpc/grpc_test_service_pb2.py @@ -2,10 +2,10 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # source: grpc_test_service.proto """Generated protocol buffer code.""" -from google.protobuf.internal import builder as _builder from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder # @@protoc_insertion_point(imports) @@ -16,12 +16,14 @@ b'\n\x17grpc_test_service.proto\x12\x10grpc_test_server"\x1f\n\x0fgRPCTestMessage\x12\x0c\n\x04text\x18\x01 \x01(\t2d\n\x0fgRPCTestService\x12Q\n\tTestServe\x12!.grpc_test_server.gRPCTestMessage\x1a!.grpc_test_server.gRPCTestMessageb\x06proto3' ) -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "grpc_test_service_pb2", globals()) +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "grpc_test_service_pb2", _globals) if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None - _GRPCTESTMESSAGE._serialized_start = 45 - _GRPCTESTMESSAGE._serialized_end = 76 - _GRPCTESTSERVICE._serialized_start = 78 - _GRPCTESTSERVICE._serialized_end = 178 + _globals["_GRPCTESTMESSAGE"]._serialized_start = 45 + _globals["_GRPCTESTMESSAGE"]._serialized_end = 76 + _globals["_GRPCTESTSERVICE"]._serialized_start = 78 + _globals["_GRPCTESTSERVICE"]._serialized_end = 178 # @@protoc_insertion_point(module_scope) diff --git a/tests/integrations/grpc/grpc_test_service_pb2.pyi b/tests/integrations/grpc/grpc_test_service_pb2.pyi index 02a0b7045b..f16d8a2d65 100644 --- a/tests/integrations/grpc/grpc_test_service_pb2.pyi +++ b/tests/integrations/grpc/grpc_test_service_pb2.pyi @@ -1,32 +1,11 @@ -""" -@generated by mypy-protobuf. Do not edit manually! -isort:skip_file -""" -import builtins -import google.protobuf.descriptor -import google.protobuf.message -import sys +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from typing import ClassVar as _ClassVar, Optional as _Optional -if sys.version_info >= (3, 8): - import typing as typing_extensions -else: - import typing_extensions +DESCRIPTOR: _descriptor.FileDescriptor -DESCRIPTOR: google.protobuf.descriptor.FileDescriptor - -@typing_extensions.final -class gRPCTestMessage(google.protobuf.message.Message): - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - TEXT_FIELD_NUMBER: builtins.int - text: builtins.str - def __init__( - self, - *, - text: builtins.str = ..., - ) -> None: ... - def ClearField( - self, field_name: typing_extensions.Literal["text", b"text"] - ) -> None: ... - -global___gRPCTestMessage = gRPCTestMessage +class gRPCTestMessage(_message.Message): + __slots__ = ["text"] + TEXT_FIELD_NUMBER: _ClassVar[int] + text: str + def __init__(self, text: _Optional[str] = ...) -> None: ... diff --git a/tests/integrations/grpc/protos/grpc_aio_test_service.proto b/tests/integrations/grpc/protos/grpc_aio_test_service.proto new file mode 100644 index 0000000000..5ed20f34e8 --- /dev/null +++ b/tests/integrations/grpc/protos/grpc_aio_test_service.proto @@ -0,0 +1,11 @@ +syntax = "proto3"; + +package grpc_aio_test_server; + +service gRPCaioTestService{ + rpc TestServe(gRPCaioTestMessage) returns (gRPCaioTestMessage); +} + +message gRPCaioTestMessage { + string text = 1; +} diff --git a/tests/integrations/grpc/grpc_test_service.proto b/tests/integrations/grpc/protos/grpc_test_service.proto similarity index 100% rename from tests/integrations/grpc/grpc_test_service.proto rename to tests/integrations/grpc/protos/grpc_test_service.proto diff --git a/tests/integrations/grpc/test_aio_grpc.py b/tests/integrations/grpc/test_grpc_aio.py similarity index 80% rename from tests/integrations/grpc/test_aio_grpc.py rename to tests/integrations/grpc/test_grpc_aio.py index ffe84e40c4..a3bbbf0067 100644 --- a/tests/integrations/grpc/test_aio_grpc.py +++ b/tests/integrations/grpc/test_grpc_aio.py @@ -11,11 +11,11 @@ from sentry_sdk import Hub, start_transaction from sentry_sdk.consts import OP from sentry_sdk.integrations.grpc import GRPCIntegration -from tests.integrations.grpc.grpc_test_service_pb2 import gRPCTestMessage -from tests.integrations.grpc.grpc_test_service_pb2_grpc import ( - gRPCTestServiceServicer, - add_gRPCTestServiceServicer_to_server, - gRPCTestServiceStub, +from tests.integrations.grpc.grpc_aio_test_service_pb2 import gRPCaioTestMessage +from tests.integrations.grpc.grpc_aio_test_service_pb2_grpc import ( + gRPCaioTestServiceServicer, + add_gRPCaioTestServiceServicer_to_server, + gRPCaioTestServiceStub, ) AIO_PORT = 50052 @@ -35,7 +35,7 @@ async def grpc_server(sentry_init, event_loop): sentry_init(traces_sample_rate=1.0, integrations=[GRPCIntegration()]) server = grpc.aio.server() server.add_insecure_port(f"[::]:{AIO_PORT}") - add_gRPCTestServiceServicer_to_server(TestService, server) + add_gRPCaioTestServiceServicer_to_server(TestService, server) await event_loop.create_task(server.start()) @@ -50,8 +50,8 @@ async def test_grpc_server_starts_transaction(capture_events, grpc_server): events = capture_events() async with grpc.aio.insecure_channel(f"localhost:{AIO_PORT}") as channel: - stub = gRPCTestServiceStub(channel) - await stub.TestServe(gRPCTestMessage(text="test")) + stub = gRPCaioTestServiceStub(channel) + await stub.TestServe(gRPCaioTestMessage(text="test")) (event,) = events span = event["spans"][0] @@ -69,7 +69,7 @@ async def test_grpc_server_continues_transaction(capture_events, grpc_server): events = capture_events() async with grpc.aio.insecure_channel(f"localhost:{AIO_PORT}") as channel: - stub = gRPCTestServiceStub(channel) + stub = gRPCaioTestServiceStub(channel) with sentry_sdk.start_transaction() as transaction: metadata = ( @@ -90,7 +90,7 @@ async def test_grpc_server_continues_transaction(capture_events, grpc_server): ), ) - await stub.TestServe(gRPCTestMessage(text="test"), metadata=metadata) + await stub.TestServe(gRPCaioTestMessage(text="test"), metadata=metadata) (event, _) = events span = event["spans"][0] @@ -110,9 +110,9 @@ async def test_grpc_server_exception(sentry_init, capture_events, grpc_server): events = capture_events() async with grpc.aio.insecure_channel(f"localhost:{AIO_PORT}") as channel: - stub = gRPCTestServiceStub(channel) + stub = gRPCaioTestServiceStub(channel) try: - await stub.TestServe(gRPCTestMessage(text="exception")) + await stub.TestServe(gRPCaioTestMessage(text="exception")) raise AssertionError() except Exception: pass @@ -130,9 +130,9 @@ async def test_grpc_client_starts_span(grpc_server, capture_events_forksafe): events = capture_events_forksafe() async with grpc.aio.insecure_channel(f"localhost:{AIO_PORT}") as channel: - stub = gRPCTestServiceStub(channel) + stub = gRPCaioTestServiceStub(channel) with start_transaction(): - await stub.TestServe(gRPCTestMessage(text="test")) + await stub.TestServe(gRPCaioTestMessage(text="test")) events.write_file.close() events.read_event() @@ -143,16 +143,16 @@ async def test_grpc_client_starts_span(grpc_server, capture_events_forksafe): assert span["op"] == OP.GRPC_CLIENT assert ( span["description"] - == "unary unary call to /grpc_test_server.gRPCTestService/TestServe" + == "unary unary call to /grpc_aio_test_server.gRPCaioTestService/TestServe" ) assert span["data"] == { "type": "unary unary", - "method": "/grpc_test_server.gRPCTestService/TestServe", + "method": "/grpc_aio_test_server.gRPCaioTestService/TestServe", "code": "OK", } -class TestService(gRPCTestServiceServicer): +class TestService(gRPCaioTestServiceServicer): class TestException(Exception): def __init__(self): super().__init__("test") @@ -166,4 +166,4 @@ async def TestServe(cls, request, context): # noqa: N802 if request.text == "exception": raise cls.TestException() - return gRPCTestMessage(text=request.text) + return gRPCaioTestMessage(text=request.text) From d4ffa1fe9669134934963ed4dcc7b8e928561316 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Tue, 3 Oct 2023 13:43:51 +0200 Subject: [PATCH 22/45] Prevent flake8 from checking generated files --- .flake8 | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.flake8 b/.flake8 index fb02f4fdef..41ada9c946 100644 --- a/.flake8 +++ b/.flake8 @@ -18,4 +18,6 @@ extend-exclude=checkouts,lol* exclude = # gRCP generated files grpc_test_service_pb2.py - grpc_test_service_pb2_grpc.py \ No newline at end of file + grpc_test_service_pb2_grpc.py + grpc_aio_test_service_pb2.py + grpc_aio_test_service_pb2_grpc.py From d73783047b567a7700b9f3fc3845bc72e235f3a0 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Tue, 3 Oct 2023 13:58:16 +0200 Subject: [PATCH 23/45] Tell black to not check auto generated files. --- sentry_sdk/pyproject.toml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 sentry_sdk/pyproject.toml diff --git a/sentry_sdk/pyproject.toml b/sentry_sdk/pyproject.toml new file mode 100644 index 0000000000..fcfed16531 --- /dev/null +++ b/sentry_sdk/pyproject.toml @@ -0,0 +1,9 @@ +[tool.black] +# 'extend-exclude' excludes files or directories in addition to the defaults +extend-exclude = ''' +# A regex preceded with ^/ will apply only to files and directories +# in the root of the project. +( + .*_pb2.py # exclude autogenerated Protocol Buffer files anywhere in the project +) +''' From 02cfbde55a5e4638b8e25612fdd5abd18bab66f5 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Tue, 3 Oct 2023 14:07:25 +0200 Subject: [PATCH 24/45] Added pyproject.toml to right directory --- sentry_sdk/pyproject.toml => pyproject.toml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename sentry_sdk/pyproject.toml => pyproject.toml (100%) diff --git a/sentry_sdk/pyproject.toml b/pyproject.toml similarity index 100% rename from sentry_sdk/pyproject.toml rename to pyproject.toml From 4bf2c832687d00f3a295dbabbc5fb86e91e022ed Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Tue, 3 Oct 2023 14:26:30 +0200 Subject: [PATCH 25/45] Fixed some linting --- sentry_sdk/integrations/grpc/__init__.py | 6 +++--- sentry_sdk/integrations/grpc/aio/client.py | 2 +- sentry_sdk/integrations/grpc/client.py | 2 +- sentry_sdk/integrations/grpc/server.py | 3 +-- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/sentry_sdk/integrations/grpc/__init__.py b/sentry_sdk/integrations/grpc/__init__.py index 125636c92f..d85860bb79 100644 --- a/sentry_sdk/integrations/grpc/__init__.py +++ b/sentry_sdk/integrations/grpc/__init__.py @@ -13,7 +13,7 @@ from .aio.server import ServerInterceptor as AsyncServerInterceptor from .aio.client import ClientInterceptor as AsyncClientInterceptor -from typing import Optional, Sequence +from typing import Any, Optional, Sequence # Hack to get new Python features working in older versions # without introducing a hard dependency on `typing_extensions` @@ -42,7 +42,7 @@ def _wrap_channel_sync(func: Callable[P, Channel]) -> Callable[P, Channel]: "Wrapper for synchronous secure and insecure channel." @wraps(func) - def patched_channel(*args, **kwargs) -> Channel: + def patched_channel(*args: Any, **kwargs: Any) -> Channel: channel = func(*args, **kwargs) return intercept_channel(channel, ClientInterceptor()) @@ -56,7 +56,7 @@ def _wrap_channel_async(func: Callable[P, AsyncChannel]) -> Callable[P, AsyncCha def patched_channel( *args, interceptors: Optional[Sequence[grpc.aio.ClientInterceptor]] = None, - **kwargs + **kwargs, ) -> Channel: interceptor = AsyncClientInterceptor() interceptors = [interceptor, *(interceptors or [])] diff --git a/sentry_sdk/integrations/grpc/aio/client.py b/sentry_sdk/integrations/grpc/aio/client.py index 908ccebcf0..ae3ca39379 100644 --- a/sentry_sdk/integrations/grpc/aio/client.py +++ b/sentry_sdk/integrations/grpc/aio/client.py @@ -36,7 +36,7 @@ async def intercept_unary_unary( @staticmethod def _update_client_call_details_metadata_from_hub( client_call_details: ClientCallDetails, hub: Hub - ): + ) -> ClientCallDetails: metadata = ( list(client_call_details.metadata) if client_call_details.metadata else [] ) diff --git a/sentry_sdk/integrations/grpc/client.py b/sentry_sdk/integrations/grpc/client.py index 1eb3621b0b..81da724298 100644 --- a/sentry_sdk/integrations/grpc/client.py +++ b/sentry_sdk/integrations/grpc/client.py @@ -11,7 +11,7 @@ from grpc import ClientCallDetails, Call from grpc._interceptor import _UnaryOutcome from grpc.aio._interceptor import UnaryStreamCall - from google.protobuf.message import Message # type: ignore + from google.protobuf.message import Message except ImportError: raise DidNotEnable("grpcio is not installed") diff --git a/sentry_sdk/integrations/grpc/server.py b/sentry_sdk/integrations/grpc/server.py index cdeea4a2fa..ea013dc788 100644 --- a/sentry_sdk/integrations/grpc/server.py +++ b/sentry_sdk/integrations/grpc/server.py @@ -6,8 +6,7 @@ if MYPY: from typing import Callable, Optional - from google.protobuf.message import Message # type: ignore - + from google.protobuf.message import Message try: import grpc from grpc import ServicerContext, HandlerCallDetails, RpcMethodHandler From 2437f875781ea2e7cc5fade1249b478270b90497 Mon Sep 17 00:00:00 2001 From: Florian Dellekart Date: Tue, 3 Oct 2023 18:34:36 +0200 Subject: [PATCH 26/45] test(gRPC): Add test for unary stream client interceptor The test is failing because the status code method of the response was causing execution to get stuck. Not sure if this worked with previous versions as the behavior was not tested before. --- sentry_sdk/integrations/grpc/client.py | 3 ++- tests/integrations/grpc/test_grpc.py | 37 ++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/grpc/client.py b/sentry_sdk/integrations/grpc/client.py index 81da724298..9c5bec2faa 100644 --- a/sentry_sdk/integrations/grpc/client.py +++ b/sentry_sdk/integrations/grpc/client.py @@ -57,7 +57,8 @@ def intercept_unary_stream(self, continuation, client_call_details, request): response = continuation( client_call_details, request ) # type: UnaryStreamCall - span.set_data("code", response.code().name) + # Setting code on unary-stream leads to execution getting stuck + # span.set_data("code", response.code().name) return response diff --git a/tests/integrations/grpc/test_grpc.py b/tests/integrations/grpc/test_grpc.py index 8208a36a62..014b59f41b 100644 --- a/tests/integrations/grpc/test_grpc.py +++ b/tests/integrations/grpc/test_grpc.py @@ -158,6 +158,38 @@ def test_grpc_client_starts_span(sentry_init, capture_events_forksafe): } +@pytest.mark.forked +def test_grpc_client_unary_stream_starts_span(sentry_init, capture_events_forksafe): + sentry_init(traces_sample_rate=1.0, integrations=[GRPCIntegration()]) + events = capture_events_forksafe() + + server = _set_up() + + with grpc.insecure_channel(f"localhost:{PORT}") as channel: + stub = gRPCTestServiceStub(channel) + + with start_transaction(): + [el for el in stub.TestUnaryStream(gRPCTestMessage(text="test"))] + + _tear_down(server=server) + + events.write_file.close() + local_transaction = events.read_event() + span = local_transaction["spans"][0] + + assert len(local_transaction["spans"]) == 1 + assert span["op"] == OP.GRPC_CLIENT + assert ( + span["description"] + == "unary stream call to /grpc_test_server.gRPCTestService/TestUnaryStream" + ) + assert span["data"] == { + "type": "unary stream", + "method": "/grpc_test_server.gRPCTestService/TestUnaryStream", + "code": "OK", + } + + # using unittest.mock.Mock not possible because grpc verifies # that the interceptor is of the correct type class MockClientInterceptor(grpc.UnaryUnaryClientInterceptor): @@ -263,3 +295,8 @@ def TestServe(request, context): # noqa: N802 pass return gRPCTestMessage(text=request.text) + + @staticmethod + def TestUnaryStream(request, context): # noqa: N802 + for _ in range(3): + yield gRPCTestMessage(text=request.text) From 30ada2f24755a4abba3e2193a5ed56e04512daa3 Mon Sep 17 00:00:00 2001 From: Florian Dellekart Date: Tue, 3 Oct 2023 19:13:40 +0200 Subject: [PATCH 27/45] refactor(gRPC): Remove external APIs from server side interceptor The method used does not work with the unary-stream interceptor and can be removed with any minor release. See: https://github.com/grpc/grpc/blob/master/src/python/grpcio/grpc/experimental/__init__.py --- sentry_sdk/integrations/grpc/aio/server.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/sentry_sdk/integrations/grpc/aio/server.py b/sentry_sdk/integrations/grpc/aio/server.py index ded1e16eba..1d1cecaf4b 100644 --- a/sentry_sdk/integrations/grpc/aio/server.py +++ b/sentry_sdk/integrations/grpc/aio/server.py @@ -1,5 +1,3 @@ -from functools import wraps - from sentry_sdk import Hub from sentry_sdk._types import MYPY from sentry_sdk.consts import OP @@ -16,7 +14,6 @@ import grpc from grpc import HandlerCallDetails, RpcMethodHandler from grpc.aio import ServicerContext - from grpc.experimental import wrap_server_method_handler except ImportError: raise DidNotEnable("grpcio is not installed") @@ -31,11 +28,7 @@ def __init__(self, find_name=None): async def intercept_service(self, continuation, handler_call_details): # type: (ServerInterceptor, Callable[[HandlerCallDetails], Awaitable[RpcMethodHandler]], HandlerCallDetails) -> Awaitable[RpcMethodHandler] handler = await continuation(handler_call_details) - return wrap_server_method_handler(self.wrapper, handler) - def wrapper(self, handler): - # type: (ServerInterceptor, Callable[[Any, ServicerContext], Awaitable[Any]]) -> Callable[[Any, ServicerContext], Awaitable[Any]] - @wraps(handler) async def wrapped(request, context): # type: (Any, ServicerContext) -> Any name = self._find_method_name(context) @@ -53,7 +46,7 @@ async def wrapped(request, context): with hub.start_transaction(transaction=transaction): try: - return await handler(request, context) + return await handler.unary_unary(request, context) except Exception as exc: event, hint = event_from_exception( exc, @@ -62,7 +55,11 @@ async def wrapped(request, context): hub.capture_event(event, hint=hint) raise - return wrapped + return grpc.unary_unary_rpc_method_handler( + wrapped, + request_deserializer=handler.request_deserializer, + response_serializer=handler.response_serializer, + ) @staticmethod def _find_name(context): From 8b3e7e63a87c7f7d9fc5bd1dc1254fc6e1b67e2f Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Wed, 4 Oct 2023 11:08:36 +0200 Subject: [PATCH 28/45] Cleaned up tests --- .flake8 | 2 - pyproject.toml | 1 + .../grpc/compile_test_services.sh | 6 +- .../grpc/grpc_aio_test_service_pb2.py | 31 -------- .../grpc/grpc_aio_test_service_pb2.pyi | 11 --- .../grpc/grpc_aio_test_service_pb2_grpc.py | 79 ------------------- .../grpc/grpc_test_service_pb2.py | 19 +++-- .../grpc/grpc_test_service_pb2_grpc.py | 63 ++++++--------- .../grpc/protos/grpc_aio_test_service.proto | 11 --- tests/integrations/grpc/test_grpc_aio.py | 36 ++++----- 10 files changed, 55 insertions(+), 204 deletions(-) delete mode 100644 tests/integrations/grpc/grpc_aio_test_service_pb2.py delete mode 100644 tests/integrations/grpc/grpc_aio_test_service_pb2.pyi delete mode 100644 tests/integrations/grpc/grpc_aio_test_service_pb2_grpc.py delete mode 100644 tests/integrations/grpc/protos/grpc_aio_test_service.proto diff --git a/.flake8 b/.flake8 index 41ada9c946..8610e09241 100644 --- a/.flake8 +++ b/.flake8 @@ -19,5 +19,3 @@ exclude = # gRCP generated files grpc_test_service_pb2.py grpc_test_service_pb2_grpc.py - grpc_aio_test_service_pb2.py - grpc_aio_test_service_pb2_grpc.py diff --git a/pyproject.toml b/pyproject.toml index fcfed16531..20ee9680f7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,5 +5,6 @@ extend-exclude = ''' # in the root of the project. ( .*_pb2.py # exclude autogenerated Protocol Buffer files anywhere in the project + | .*_pb2_grpc.py # exclude autogenerated Protocol Buffer files anywhere in the project ) ''' diff --git a/tests/integrations/grpc/compile_test_services.sh b/tests/integrations/grpc/compile_test_services.sh index d244008048..34c881b001 100755 --- a/tests/integrations/grpc/compile_test_services.sh +++ b/tests/integrations/grpc/compile_test_services.sh @@ -1,9 +1,7 @@ #!/usr/bin/env bash -# Create python files +# Create python file python -m grpc_tools.protoc --proto_path=./protos --python_out=. --pyi_out=. --grpc_python_out=. ./protos/grpc_test_service.proto -python -m grpc_tools.protoc --proto_path=./protos --python_out=. --pyi_out=. --grpc_python_out=. ./protos/grpc_aio_test_service.proto -# Fix imports in generated files +# Fix imports in generated file sed -i '' 's/import grpc_/import tests\.integrations\.grpc\.grpc_/g' ./grpc_test_service_pb2_grpc.py -sed -i '' 's/import grpc_/import tests\.integrations\.grpc\.grpc_/g' ./grpc_aio_test_service_pb2_grpc.py diff --git a/tests/integrations/grpc/grpc_aio_test_service_pb2.py b/tests/integrations/grpc/grpc_aio_test_service_pb2.py deleted file mode 100644 index 338c5ec5b8..0000000000 --- a/tests/integrations/grpc/grpc_aio_test_service_pb2.py +++ /dev/null @@ -1,31 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: grpc_aio_test_service.proto -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder - -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( - b'\n\x1bgrpc_aio_test_service.proto\x12\x14grpc_aio_test_server""\n\x12gRPCaioTestMessage\x12\x0c\n\x04text\x18\x01 \x01(\t2u\n\x12gRPCaioTestService\x12_\n\tTestServe\x12(.grpc_aio_test_server.gRPCaioTestMessage\x1a(.grpc_aio_test_server.gRPCaioTestMessageb\x06proto3' -) - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages( - DESCRIPTOR, "grpc_aio_test_service_pb2", _globals -) -if _descriptor._USE_C_DESCRIPTORS == False: - - DESCRIPTOR._options = None - _globals["_GRPCAIOTESTMESSAGE"]._serialized_start = 53 - _globals["_GRPCAIOTESTMESSAGE"]._serialized_end = 87 - _globals["_GRPCAIOTESTSERVICE"]._serialized_start = 89 - _globals["_GRPCAIOTESTSERVICE"]._serialized_end = 206 -# @@protoc_insertion_point(module_scope) diff --git a/tests/integrations/grpc/grpc_aio_test_service_pb2.pyi b/tests/integrations/grpc/grpc_aio_test_service_pb2.pyi deleted file mode 100644 index 13c717d82e..0000000000 --- a/tests/integrations/grpc/grpc_aio_test_service_pb2.pyi +++ /dev/null @@ -1,11 +0,0 @@ -from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from typing import ClassVar as _ClassVar, Optional as _Optional - -DESCRIPTOR: _descriptor.FileDescriptor - -class gRPCaioTestMessage(_message.Message): - __slots__ = ["text"] - TEXT_FIELD_NUMBER: _ClassVar[int] - text: str - def __init__(self, text: _Optional[str] = ...) -> None: ... diff --git a/tests/integrations/grpc/grpc_aio_test_service_pb2_grpc.py b/tests/integrations/grpc/grpc_aio_test_service_pb2_grpc.py deleted file mode 100644 index 61e7d70b6e..0000000000 --- a/tests/integrations/grpc/grpc_aio_test_service_pb2_grpc.py +++ /dev/null @@ -1,79 +0,0 @@ -# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! -"""Client and server classes corresponding to protobuf-defined services.""" -import grpc - -import tests.integrations.grpc.grpc_aio_test_service_pb2 as grpc__aio__test__service__pb2 - - -class gRPCaioTestServiceStub(object): - """Missing associated documentation comment in .proto file.""" - - def __init__(self, channel): - """Constructor. - - Args: - channel: A grpc.Channel. - """ - self.TestServe = channel.unary_unary( - "/grpc_aio_test_server.gRPCaioTestService/TestServe", - request_serializer=grpc__aio__test__service__pb2.gRPCaioTestMessage.SerializeToString, - response_deserializer=grpc__aio__test__service__pb2.gRPCaioTestMessage.FromString, - ) - - -class gRPCaioTestServiceServicer(object): - """Missing associated documentation comment in .proto file.""" - - def TestServe(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details("Method not implemented!") - raise NotImplementedError("Method not implemented!") - - -def add_gRPCaioTestServiceServicer_to_server(servicer, server): - rpc_method_handlers = { - "TestServe": grpc.unary_unary_rpc_method_handler( - servicer.TestServe, - request_deserializer=grpc__aio__test__service__pb2.gRPCaioTestMessage.FromString, - response_serializer=grpc__aio__test__service__pb2.gRPCaioTestMessage.SerializeToString, - ), - } - generic_handler = grpc.method_handlers_generic_handler( - "grpc_aio_test_server.gRPCaioTestService", rpc_method_handlers - ) - server.add_generic_rpc_handlers((generic_handler,)) - - -# This class is part of an EXPERIMENTAL API. -class gRPCaioTestService(object): - """Missing associated documentation comment in .proto file.""" - - @staticmethod - def TestServe( - request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None, - ): - return grpc.experimental.unary_unary( - request, - target, - "/grpc_aio_test_server.gRPCaioTestService/TestServe", - grpc__aio__test__service__pb2.gRPCaioTestMessage.SerializeToString, - grpc__aio__test__service__pb2.gRPCaioTestMessage.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - ) diff --git a/tests/integrations/grpc/grpc_test_service_pb2.py b/tests/integrations/grpc/grpc_test_service_pb2.py index 718fbc0081..ab5ecd0d11 100644 --- a/tests/integrations/grpc/grpc_test_service_pb2.py +++ b/tests/integrations/grpc/grpc_test_service_pb2.py @@ -6,24 +6,23 @@ from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database from google.protobuf.internal import builder as _builder - # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( - b'\n\x17grpc_test_service.proto\x12\x10grpc_test_server"\x1f\n\x0fgRPCTestMessage\x12\x0c\n\x04text\x18\x01 \x01(\t2d\n\x0fgRPCTestService\x12Q\n\tTestServe\x12!.grpc_test_server.gRPCTestMessage\x1a!.grpc_test_server.gRPCTestMessageb\x06proto3' -) + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x17grpc_test_service.proto\x12\x10grpc_test_server\"\x1f\n\x0fgRPCTestMessage\x12\x0c\n\x04text\x18\x01 \x01(\t2d\n\x0fgRPCTestService\x12Q\n\tTestServe\x12!.grpc_test_server.gRPCTestMessage\x1a!.grpc_test_server.gRPCTestMessageb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "grpc_test_service_pb2", _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'grpc_test_service_pb2', _globals) if _descriptor._USE_C_DESCRIPTORS == False: - DESCRIPTOR._options = None - _globals["_GRPCTESTMESSAGE"]._serialized_start = 45 - _globals["_GRPCTESTMESSAGE"]._serialized_end = 76 - _globals["_GRPCTESTSERVICE"]._serialized_start = 78 - _globals["_GRPCTESTSERVICE"]._serialized_end = 178 + DESCRIPTOR._options = None + _globals['_GRPCTESTMESSAGE']._serialized_start=45 + _globals['_GRPCTESTMESSAGE']._serialized_end=76 + _globals['_GRPCTESTSERVICE']._serialized_start=78 + _globals['_GRPCTESTSERVICE']._serialized_end=178 # @@protoc_insertion_point(module_scope) diff --git a/tests/integrations/grpc/grpc_test_service_pb2_grpc.py b/tests/integrations/grpc/grpc_test_service_pb2_grpc.py index 73b7d94c16..582835781f 100644 --- a/tests/integrations/grpc/grpc_test_service_pb2_grpc.py +++ b/tests/integrations/grpc/grpc_test_service_pb2_grpc.py @@ -15,10 +15,10 @@ def __init__(self, channel): channel: A grpc.Channel. """ self.TestServe = channel.unary_unary( - "/grpc_test_server.gRPCTestService/TestServe", - request_serializer=grpc__test__service__pb2.gRPCTestMessage.SerializeToString, - response_deserializer=grpc__test__service__pb2.gRPCTestMessage.FromString, - ) + '/grpc_test_server.gRPCTestService/TestServe', + request_serializer=grpc__test__service__pb2.gRPCTestMessage.SerializeToString, + response_deserializer=grpc__test__service__pb2.gRPCTestMessage.FromString, + ) class gRPCTestServiceServicer(object): @@ -27,53 +27,40 @@ class gRPCTestServiceServicer(object): def TestServe(self, request, context): """Missing associated documentation comment in .proto file.""" context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details("Method not implemented!") - raise NotImplementedError("Method not implemented!") + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') def add_gRPCTestServiceServicer_to_server(servicer, server): rpc_method_handlers = { - "TestServe": grpc.unary_unary_rpc_method_handler( - servicer.TestServe, - request_deserializer=grpc__test__service__pb2.gRPCTestMessage.FromString, - response_serializer=grpc__test__service__pb2.gRPCTestMessage.SerializeToString, - ), + 'TestServe': grpc.unary_unary_rpc_method_handler( + servicer.TestServe, + request_deserializer=grpc__test__service__pb2.gRPCTestMessage.FromString, + response_serializer=grpc__test__service__pb2.gRPCTestMessage.SerializeToString, + ), } generic_handler = grpc.method_handlers_generic_handler( - "grpc_test_server.gRPCTestService", rpc_method_handlers - ) + 'grpc_test_server.gRPCTestService', rpc_method_handlers) server.add_generic_rpc_handlers((generic_handler,)) -# This class is part of an EXPERIMENTAL API. + # This class is part of an EXPERIMENTAL API. class gRPCTestService(object): """Missing associated documentation comment in .proto file.""" @staticmethod - def TestServe( - request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None, - ): - return grpc.experimental.unary_unary( - request, + def TestServe(request, target, - "/grpc_test_server.gRPCTestService/TestServe", + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/grpc_test_server.gRPCTestService/TestServe', grpc__test__service__pb2.gRPCTestMessage.SerializeToString, grpc__test__service__pb2.gRPCTestMessage.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - ) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) diff --git a/tests/integrations/grpc/protos/grpc_aio_test_service.proto b/tests/integrations/grpc/protos/grpc_aio_test_service.proto deleted file mode 100644 index 5ed20f34e8..0000000000 --- a/tests/integrations/grpc/protos/grpc_aio_test_service.proto +++ /dev/null @@ -1,11 +0,0 @@ -syntax = "proto3"; - -package grpc_aio_test_server; - -service gRPCaioTestService{ - rpc TestServe(gRPCaioTestMessage) returns (gRPCaioTestMessage); -} - -message gRPCaioTestMessage { - string text = 1; -} diff --git a/tests/integrations/grpc/test_grpc_aio.py b/tests/integrations/grpc/test_grpc_aio.py index a3bbbf0067..ffe84e40c4 100644 --- a/tests/integrations/grpc/test_grpc_aio.py +++ b/tests/integrations/grpc/test_grpc_aio.py @@ -11,11 +11,11 @@ from sentry_sdk import Hub, start_transaction from sentry_sdk.consts import OP from sentry_sdk.integrations.grpc import GRPCIntegration -from tests.integrations.grpc.grpc_aio_test_service_pb2 import gRPCaioTestMessage -from tests.integrations.grpc.grpc_aio_test_service_pb2_grpc import ( - gRPCaioTestServiceServicer, - add_gRPCaioTestServiceServicer_to_server, - gRPCaioTestServiceStub, +from tests.integrations.grpc.grpc_test_service_pb2 import gRPCTestMessage +from tests.integrations.grpc.grpc_test_service_pb2_grpc import ( + gRPCTestServiceServicer, + add_gRPCTestServiceServicer_to_server, + gRPCTestServiceStub, ) AIO_PORT = 50052 @@ -35,7 +35,7 @@ async def grpc_server(sentry_init, event_loop): sentry_init(traces_sample_rate=1.0, integrations=[GRPCIntegration()]) server = grpc.aio.server() server.add_insecure_port(f"[::]:{AIO_PORT}") - add_gRPCaioTestServiceServicer_to_server(TestService, server) + add_gRPCTestServiceServicer_to_server(TestService, server) await event_loop.create_task(server.start()) @@ -50,8 +50,8 @@ async def test_grpc_server_starts_transaction(capture_events, grpc_server): events = capture_events() async with grpc.aio.insecure_channel(f"localhost:{AIO_PORT}") as channel: - stub = gRPCaioTestServiceStub(channel) - await stub.TestServe(gRPCaioTestMessage(text="test")) + stub = gRPCTestServiceStub(channel) + await stub.TestServe(gRPCTestMessage(text="test")) (event,) = events span = event["spans"][0] @@ -69,7 +69,7 @@ async def test_grpc_server_continues_transaction(capture_events, grpc_server): events = capture_events() async with grpc.aio.insecure_channel(f"localhost:{AIO_PORT}") as channel: - stub = gRPCaioTestServiceStub(channel) + stub = gRPCTestServiceStub(channel) with sentry_sdk.start_transaction() as transaction: metadata = ( @@ -90,7 +90,7 @@ async def test_grpc_server_continues_transaction(capture_events, grpc_server): ), ) - await stub.TestServe(gRPCaioTestMessage(text="test"), metadata=metadata) + await stub.TestServe(gRPCTestMessage(text="test"), metadata=metadata) (event, _) = events span = event["spans"][0] @@ -110,9 +110,9 @@ async def test_grpc_server_exception(sentry_init, capture_events, grpc_server): events = capture_events() async with grpc.aio.insecure_channel(f"localhost:{AIO_PORT}") as channel: - stub = gRPCaioTestServiceStub(channel) + stub = gRPCTestServiceStub(channel) try: - await stub.TestServe(gRPCaioTestMessage(text="exception")) + await stub.TestServe(gRPCTestMessage(text="exception")) raise AssertionError() except Exception: pass @@ -130,9 +130,9 @@ async def test_grpc_client_starts_span(grpc_server, capture_events_forksafe): events = capture_events_forksafe() async with grpc.aio.insecure_channel(f"localhost:{AIO_PORT}") as channel: - stub = gRPCaioTestServiceStub(channel) + stub = gRPCTestServiceStub(channel) with start_transaction(): - await stub.TestServe(gRPCaioTestMessage(text="test")) + await stub.TestServe(gRPCTestMessage(text="test")) events.write_file.close() events.read_event() @@ -143,16 +143,16 @@ async def test_grpc_client_starts_span(grpc_server, capture_events_forksafe): assert span["op"] == OP.GRPC_CLIENT assert ( span["description"] - == "unary unary call to /grpc_aio_test_server.gRPCaioTestService/TestServe" + == "unary unary call to /grpc_test_server.gRPCTestService/TestServe" ) assert span["data"] == { "type": "unary unary", - "method": "/grpc_aio_test_server.gRPCaioTestService/TestServe", + "method": "/grpc_test_server.gRPCTestService/TestServe", "code": "OK", } -class TestService(gRPCaioTestServiceServicer): +class TestService(gRPCTestServiceServicer): class TestException(Exception): def __init__(self): super().__init__("test") @@ -166,4 +166,4 @@ async def TestServe(cls, request, context): # noqa: N802 if request.text == "exception": raise cls.TestException() - return gRPCaioTestMessage(text=request.text) + return gRPCTestMessage(text=request.text) From 9aba63ebefe825e95140b376a5401b750b1c9cdd Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Wed, 4 Oct 2023 11:21:48 +0200 Subject: [PATCH 29/45] Updated black config to work with pre-commit --- .pre-commit-config.yaml | 1 + tests/integrations/grpc/grpc_test_service_pb2.py | 3 +++ tests/integrations/grpc/grpc_test_service_pb2_grpc.py | 3 +++ 3 files changed, 7 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cb7882d38f..7e2812bc54 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,6 +11,7 @@ repos: rev: 22.6.0 hooks: - id: black + exclude: ^(.*_pb2.py|.*_pb2_grpc.py) - repo: https://github.com/pycqa/flake8 rev: 5.0.4 diff --git a/tests/integrations/grpc/grpc_test_service_pb2.py b/tests/integrations/grpc/grpc_test_service_pb2.py index ab5ecd0d11..b53e434985 100644 --- a/tests/integrations/grpc/grpc_test_service_pb2.py +++ b/tests/integrations/grpc/grpc_test_service_pb2.py @@ -13,6 +13,9 @@ + + + DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x17grpc_test_service.proto\x12\x10grpc_test_server\"\x1f\n\x0fgRPCTestMessage\x12\x0c\n\x04text\x18\x01 \x01(\t2d\n\x0fgRPCTestService\x12Q\n\tTestServe\x12!.grpc_test_server.gRPCTestMessage\x1a!.grpc_test_server.gRPCTestMessageb\x06proto3') _globals = globals() diff --git a/tests/integrations/grpc/grpc_test_service_pb2_grpc.py b/tests/integrations/grpc/grpc_test_service_pb2_grpc.py index 582835781f..6f0d22a0d0 100644 --- a/tests/integrations/grpc/grpc_test_service_pb2_grpc.py +++ b/tests/integrations/grpc/grpc_test_service_pb2_grpc.py @@ -21,6 +21,9 @@ def __init__(self, channel): ) + + + class gRPCTestServiceServicer(object): """Missing associated documentation comment in .proto file.""" From b625b2db6b43a8be302c5cf96ca70064aa0cbda1 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Wed, 4 Oct 2023 11:22:16 +0200 Subject: [PATCH 30/45] cleanup --- tests/integrations/grpc/grpc_test_service_pb2.py | 3 --- tests/integrations/grpc/grpc_test_service_pb2_grpc.py | 3 --- 2 files changed, 6 deletions(-) diff --git a/tests/integrations/grpc/grpc_test_service_pb2.py b/tests/integrations/grpc/grpc_test_service_pb2.py index b53e434985..ab5ecd0d11 100644 --- a/tests/integrations/grpc/grpc_test_service_pb2.py +++ b/tests/integrations/grpc/grpc_test_service_pb2.py @@ -13,9 +13,6 @@ - - - DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x17grpc_test_service.proto\x12\x10grpc_test_server\"\x1f\n\x0fgRPCTestMessage\x12\x0c\n\x04text\x18\x01 \x01(\t2d\n\x0fgRPCTestService\x12Q\n\tTestServe\x12!.grpc_test_server.gRPCTestMessage\x1a!.grpc_test_server.gRPCTestMessageb\x06proto3') _globals = globals() diff --git a/tests/integrations/grpc/grpc_test_service_pb2_grpc.py b/tests/integrations/grpc/grpc_test_service_pb2_grpc.py index 6f0d22a0d0..582835781f 100644 --- a/tests/integrations/grpc/grpc_test_service_pb2_grpc.py +++ b/tests/integrations/grpc/grpc_test_service_pb2_grpc.py @@ -21,9 +21,6 @@ def __init__(self, channel): ) - - - class gRPCTestServiceServicer(object): """Missing associated documentation comment in .proto file.""" From 9c9f3d8dd50c277a42263f7593bbf1e19ffde815 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Wed, 4 Oct 2023 11:23:25 +0200 Subject: [PATCH 31/45] cleanup --- linter-requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/linter-requirements.txt b/linter-requirements.txt index d5c4a1fd5d..289df0cd7f 100644 --- a/linter-requirements.txt +++ b/linter-requirements.txt @@ -10,4 +10,3 @@ loguru # There is no separate types module. flake8-bugbear pep8-naming pre-commit # local linting -types-protobuf From 2276ee451e9ae65a71bfe90f4270ba9d0bcf6c6e Mon Sep 17 00:00:00 2001 From: Florian Dellekart Date: Fri, 6 Oct 2023 15:41:49 +0200 Subject: [PATCH 32/45] refactor: gRPC code generation script to be run from project root I am not sure if the previous version was on purpose only running from inside the test directory, however, I think this is more convenient because the project root is the usual PWD during development. Furthermore, I append the test directory to sys.path so we don't need to tamper with the gRPC autogenerated code for the imports to work. --- tests/integrations/grpc/__init__.py | 5 +++++ tests/integrations/grpc/compile_test_services.sh | 14 +++++++++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/tests/integrations/grpc/__init__.py b/tests/integrations/grpc/__init__.py index 88a0a201e4..f18dce91e2 100644 --- a/tests/integrations/grpc/__init__.py +++ b/tests/integrations/grpc/__init__.py @@ -1,3 +1,8 @@ +import sys +from pathlib import Path + import pytest +# For imports inside gRPC autogenerated code to work +sys.path.append(str(Path(__file__).parent)) pytest.importorskip("grpc") diff --git a/tests/integrations/grpc/compile_test_services.sh b/tests/integrations/grpc/compile_test_services.sh index 34c881b001..777a27e6e5 100755 --- a/tests/integrations/grpc/compile_test_services.sh +++ b/tests/integrations/grpc/compile_test_services.sh @@ -1,7 +1,15 @@ #!/usr/bin/env bash +# Run this script from the project root to generate the python code + +TARGET_PATH=./tests/integrations/grpc + # Create python file -python -m grpc_tools.protoc --proto_path=./protos --python_out=. --pyi_out=. --grpc_python_out=. ./protos/grpc_test_service.proto +python -m grpc_tools.protoc \ + --proto_path=$TARGET_PATH/protos/ \ + --python_out=$TARGET_PATH/ \ + --pyi_out=$TARGET_PATH/ \ + --grpc_python_out=$TARGET_PATH/ \ + $TARGET_PATH/protos/grpc_test_service.proto -# Fix imports in generated file -sed -i '' 's/import grpc_/import tests\.integrations\.grpc\.grpc_/g' ./grpc_test_service_pb2_grpc.py +echo Code generation successfull From 119e415e3d820e1b15fcbff7689ab829452a5c3f Mon Sep 17 00:00:00 2001 From: Florian Dellekart Date: Fri, 6 Oct 2023 15:44:51 +0200 Subject: [PATCH 33/45] test: Add test endpoint for unary-stream gRPC method --- .../grpc/grpc_test_service_pb2.py | 7 ++-- .../grpc/grpc_test_service_pb2_grpc.py | 35 ++++++++++++++++++- .../grpc/protos/grpc_test_service.proto | 1 + 3 files changed, 38 insertions(+), 5 deletions(-) diff --git a/tests/integrations/grpc/grpc_test_service_pb2.py b/tests/integrations/grpc/grpc_test_service_pb2.py index ab5ecd0d11..ba33818875 100644 --- a/tests/integrations/grpc/grpc_test_service_pb2.py +++ b/tests/integrations/grpc/grpc_test_service_pb2.py @@ -13,16 +13,15 @@ -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x17grpc_test_service.proto\x12\x10grpc_test_server\"\x1f\n\x0fgRPCTestMessage\x12\x0c\n\x04text\x18\x01 \x01(\t2d\n\x0fgRPCTestService\x12Q\n\tTestServe\x12!.grpc_test_server.gRPCTestMessage\x1a!.grpc_test_server.gRPCTestMessageb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x17grpc_test_service.proto\x12\x10grpc_test_server\"\x1f\n\x0fgRPCTestMessage\x12\x0c\n\x04text\x18\x01 \x01(\t2\xbf\x01\n\x0fgRPCTestService\x12Q\n\tTestServe\x12!.grpc_test_server.gRPCTestMessage\x1a!.grpc_test_server.gRPCTestMessage\x12Y\n\x0fTestUnaryStream\x12!.grpc_test_server.gRPCTestMessage\x1a!.grpc_test_server.gRPCTestMessage0\x01\x62\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'grpc_test_service_pb2', _globals) if _descriptor._USE_C_DESCRIPTORS == False: - DESCRIPTOR._options = None _globals['_GRPCTESTMESSAGE']._serialized_start=45 _globals['_GRPCTESTMESSAGE']._serialized_end=76 - _globals['_GRPCTESTSERVICE']._serialized_start=78 - _globals['_GRPCTESTSERVICE']._serialized_end=178 + _globals['_GRPCTESTSERVICE']._serialized_start=79 + _globals['_GRPCTESTSERVICE']._serialized_end=270 # @@protoc_insertion_point(module_scope) diff --git a/tests/integrations/grpc/grpc_test_service_pb2_grpc.py b/tests/integrations/grpc/grpc_test_service_pb2_grpc.py index 582835781f..3f7a6098d3 100644 --- a/tests/integrations/grpc/grpc_test_service_pb2_grpc.py +++ b/tests/integrations/grpc/grpc_test_service_pb2_grpc.py @@ -2,7 +2,7 @@ """Client and server classes corresponding to protobuf-defined services.""" import grpc -import tests.integrations.grpc.grpc_test_service_pb2 as grpc__test__service__pb2 +import grpc_test_service_pb2 as grpc__test__service__pb2 class gRPCTestServiceStub(object): @@ -19,6 +19,11 @@ def __init__(self, channel): request_serializer=grpc__test__service__pb2.gRPCTestMessage.SerializeToString, response_deserializer=grpc__test__service__pb2.gRPCTestMessage.FromString, ) + self.TestUnaryStream = channel.unary_stream( + '/grpc_test_server.gRPCTestService/TestUnaryStream', + request_serializer=grpc__test__service__pb2.gRPCTestMessage.SerializeToString, + response_deserializer=grpc__test__service__pb2.gRPCTestMessage.FromString, + ) class gRPCTestServiceServicer(object): @@ -30,6 +35,12 @@ def TestServe(self, request, context): context.set_details('Method not implemented!') raise NotImplementedError('Method not implemented!') + def TestUnaryStream(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + def add_gRPCTestServiceServicer_to_server(servicer, server): rpc_method_handlers = { @@ -38,6 +49,11 @@ def add_gRPCTestServiceServicer_to_server(servicer, server): request_deserializer=grpc__test__service__pb2.gRPCTestMessage.FromString, response_serializer=grpc__test__service__pb2.gRPCTestMessage.SerializeToString, ), + 'TestUnaryStream': grpc.unary_stream_rpc_method_handler( + servicer.TestUnaryStream, + request_deserializer=grpc__test__service__pb2.gRPCTestMessage.FromString, + response_serializer=grpc__test__service__pb2.gRPCTestMessage.SerializeToString, + ), } generic_handler = grpc.method_handlers_generic_handler( 'grpc_test_server.gRPCTestService', rpc_method_handlers) @@ -64,3 +80,20 @@ def TestServe(request, grpc__test__service__pb2.gRPCTestMessage.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def TestUnaryStream(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_stream(request, target, '/grpc_test_server.gRPCTestService/TestUnaryStream', + grpc__test__service__pb2.gRPCTestMessage.SerializeToString, + grpc__test__service__pb2.gRPCTestMessage.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) diff --git a/tests/integrations/grpc/protos/grpc_test_service.proto b/tests/integrations/grpc/protos/grpc_test_service.proto index 43497c7129..aafa75cf53 100644 --- a/tests/integrations/grpc/protos/grpc_test_service.proto +++ b/tests/integrations/grpc/protos/grpc_test_service.proto @@ -4,6 +4,7 @@ package grpc_test_server; service gRPCTestService{ rpc TestServe(gRPCTestMessage) returns (gRPCTestMessage); + rpc TestUnaryStream(gRPCTestMessage) returns (stream gRPCTestMessage); } message gRPCTestMessage { From c465dcfd9eadee242c2f9a218a946224e470e1c6 Mon Sep 17 00:00:00 2001 From: Florian Dellekart Date: Fri, 6 Oct 2023 15:48:34 +0200 Subject: [PATCH 34/45] fix: Typos in interceptors and status code of stream response stuck for async too --- sentry_sdk/integrations/grpc/aio/client.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sentry_sdk/integrations/grpc/aio/client.py b/sentry_sdk/integrations/grpc/aio/client.py index 9c7f13db8a..e2f5e54e0a 100644 --- a/sentry_sdk/integrations/grpc/aio/client.py +++ b/sentry_sdk/integrations/grpc/aio/client.py @@ -75,17 +75,17 @@ async def intercept_unary_stream( method = client_call_details.method with hub.start_span( - op=OP.GRPC_CLIENT, description="unary stream call %s" % method + op=OP.GRPC_CLIENT, description="unary stream call to %s" % method.decode() ) as span: span.set_data("type", "unary stream") - span.set_data("methdo", method) + span.set_data("method", method) client_call_details = self._update_client_call_details_metadata_from_hub( client_call_details, hub ) response = await continuation(client_call_details, request) - status_code = await response.code() - span.set_data("code", status_code) + # status_code = await response.code() + # span.set_data("code", status_code) return response From aec6c7c885d135e13fda3d86af53b9a260ebd1b6 Mon Sep 17 00:00:00 2001 From: Florian Dellekart Date: Fri, 6 Oct 2023 15:50:49 +0200 Subject: [PATCH 35/45] test: Add test for async unary-stream client interceptor --- tests/integrations/grpc/test_grpc_aio.py | 38 ++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/tests/integrations/grpc/test_grpc_aio.py b/tests/integrations/grpc/test_grpc_aio.py index ffe84e40c4..ed81ee0f5c 100644 --- a/tests/integrations/grpc/test_grpc_aio.py +++ b/tests/integrations/grpc/test_grpc_aio.py @@ -106,7 +106,6 @@ async def test_grpc_server_continues_transaction(capture_events, grpc_server): @pytest.mark.asyncio async def test_grpc_server_exception(sentry_init, capture_events, grpc_server): - sentry_init(traces_sample_rate=1.0) events = capture_events() async with grpc.aio.insecure_channel(f"localhost:{AIO_PORT}") as channel: @@ -126,7 +125,9 @@ async def test_grpc_server_exception(sentry_init, capture_events, grpc_server): @pytest.mark.asyncio -async def test_grpc_client_starts_span(grpc_server, capture_events_forksafe): +async def test_grpc_client_starts_span( + grpc_server, sentry_init, capture_events_forksafe +): events = capture_events_forksafe() async with grpc.aio.insecure_channel(f"localhost:{AIO_PORT}") as channel: @@ -152,6 +153,34 @@ async def test_grpc_client_starts_span(grpc_server, capture_events_forksafe): } +@pytest.mark.asyncio +async def test_grpc_client_unary_stream_starts_span( + grpc_server, capture_events_forksafe +): + events = capture_events_forksafe() + + async with grpc.aio.insecure_channel(f"localhost:{AIO_PORT}") as channel: + stub = gRPCTestServiceStub(channel) + with start_transaction(): + response = stub.TestUnaryStream(gRPCTestMessage(text="test")) + [_ async for _ in response] + + events.write_file.close() + local_transaction = events.read_event() + span = local_transaction["spans"][0] + + assert len(local_transaction["spans"]) == 1 + assert span["op"] == OP.GRPC_CLIENT + assert ( + span["description"] + == "unary stream call to /grpc_test_server.gRPCTestService/TestUnaryStream" + ) + assert span["data"] == { + "type": "unary stream", + "method": "/grpc_test_server.gRPCTestService/TestUnaryStream", + } + + class TestService(gRPCTestServiceServicer): class TestException(Exception): def __init__(self): @@ -167,3 +196,8 @@ async def TestServe(cls, request, context): # noqa: N802 raise cls.TestException() return gRPCTestMessage(text=request.text) + + @classmethod + async def TestUnaryStream(cls, request, context): # noqa: N802 + for _ in range(3): + yield gRPCTestMessage(text=request.text) From 7b09a67b7ecf53e7d716050ec8092db96563c1cf Mon Sep 17 00:00:00 2001 From: Florian Dellekart Date: Fri, 6 Oct 2023 16:45:25 +0200 Subject: [PATCH 36/45] fix(gRPC): Make sure that server side interceptor does not break unary stream request --- sentry_sdk/integrations/grpc/aio/server.py | 66 +++++++++++++--------- 1 file changed, 39 insertions(+), 27 deletions(-) diff --git a/sentry_sdk/integrations/grpc/aio/server.py b/sentry_sdk/integrations/grpc/aio/server.py index 1d1cecaf4b..b06ecb4a30 100644 --- a/sentry_sdk/integrations/grpc/aio/server.py +++ b/sentry_sdk/integrations/grpc/aio/server.py @@ -29,33 +29,45 @@ async def intercept_service(self, continuation, handler_call_details): # type: (ServerInterceptor, Callable[[HandlerCallDetails], Awaitable[RpcMethodHandler]], HandlerCallDetails) -> Awaitable[RpcMethodHandler] handler = await continuation(handler_call_details) - async def wrapped(request, context): - # type: (Any, ServicerContext) -> Any - name = self._find_method_name(context) - if not name: - return await handler(request, context) - - hub = Hub(Hub.current) - - transaction = Transaction.continue_from_headers( - dict(context.invocation_metadata()), - op=OP.GRPC_SERVER, - name=name, - source=TRANSACTION_SOURCE_CUSTOM, - ) - - with hub.start_transaction(transaction=transaction): - try: - return await handler.unary_unary(request, context) - except Exception as exc: - event, hint = event_from_exception( - exc, - mechanism={"type": "grpc", "handled": False}, - ) - hub.capture_event(event, hint=hint) - raise - - return grpc.unary_unary_rpc_method_handler( + if not handler.request_streaming and not handler.response_streaming: + handler_factory = grpc.unary_unary_rpc_method_handler + + async def wrapped(request, context): + # type: (Any, ServicerContext) -> Any + name = self._find_method_name(context) + if not name: + return await handler(request, context) + + hub = Hub.current + + # What if the headers are empty? + transaction = Transaction.continue_from_headers( + dict(context.invocation_metadata()), + op=OP.GRPC_SERVER, + name=name, + source=TRANSACTION_SOURCE_CUSTOM, + ) + + with hub.start_transaction(transaction=transaction): + try: + return await handler.unary_unary(request, context) + except Exception as exc: + event, hint = event_from_exception( + exc, + mechanism={"type": "grpc", "handled": False}, + ) + hub.capture_event(event, hint=hint) + raise + + elif not handler.request_streaming and handler.response_streaming: + handler_factory = grpc.unary_stream_rpc_method_handler + + async def wrapped(request, context): # type: ignore + # type: (Any, ServicerContext) -> Any + async for r in handler.unary_stream(request, context): + yield r + + return handler_factory( wrapped, request_deserializer=handler.request_deserializer, response_serializer=handler.response_serializer, From 6a197371b7dc0e207517a90796cdd27d9ec8ebad Mon Sep 17 00:00:00 2001 From: Florian Dellekart Date: Fri, 6 Oct 2023 16:46:29 +0200 Subject: [PATCH 37/45] feat(gRPC): Ensure backwards compatibility in case gRPC interceptors are used together with integration --- sentry_sdk/integrations/grpc/__init__.py | 32 +++++++++++++++++++++++- sentry_sdk/integrations/grpc/client.py | 2 ++ sentry_sdk/integrations/grpc/server.py | 2 +- tests/integrations/grpc/test_grpc.py | 1 - 4 files changed, 34 insertions(+), 3 deletions(-) diff --git a/sentry_sdk/integrations/grpc/__init__.py b/sentry_sdk/integrations/grpc/__init__.py index 16d1bff9ce..111138ab07 100644 --- a/sentry_sdk/integrations/grpc/__init__.py +++ b/sentry_sdk/integrations/grpc/__init__.py @@ -49,11 +49,35 @@ def _wrap_channel_sync(func: Callable[P, Channel]) -> Callable[P, Channel]: @wraps(func) def patched_channel(*args: Any, **kwargs: Any) -> Channel: channel = func(*args, **kwargs) - return intercept_channel(channel, ClientInterceptor()) + if not ClientInterceptor._is_intercepted: + ClientInterceptor._is_intercepted = True + return intercept_channel(channel, ClientInterceptor()) + else: + return channel return patched_channel +def _wrap_intercept_channel(func: Callable[P, Channel]) -> Callable[P, Channel]: + @wraps(func) + def patched_intercept_channel( + channel: Channel, *interceptors: grpc.ServerInterceptor + ) -> Channel: + if ClientInterceptor._is_intercepted: + interceptors = tuple( + [ + interceptor + for interceptor in interceptors + if not isinstance(interceptor, ClientInterceptor) + ] + ) + else: + interceptors = interceptors + return intercept_channel(channel, *interceptors) + + return patched_intercept_channel # type: ignore + + def _wrap_channel_async(func: Callable[P, AsyncChannel]) -> Callable[P, AsyncChannel]: "Wrapper for asynchronous secure and insecure channel." @@ -82,6 +106,11 @@ def patched_server( interceptors: Optional[Sequence[grpc.ServerInterceptor]] = None, **kwargs: P.kwargs, ) -> Server: + interceptors = [ + interceptor + for interceptor in interceptors or [] + if not isinstance(interceptor, ServerInterceptor) + ] server_interceptor = ServerInterceptor() interceptors = [server_interceptor, *(interceptors or [])] return func(*args, interceptors=interceptors, **kwargs) # type: ignore @@ -116,6 +145,7 @@ def setup_once() -> None: grpc.insecure_channel = _wrap_channel_sync(grpc.insecure_channel) grpc.secure_channel = _wrap_channel_sync(grpc.secure_channel) + grpc.intercept_channel = _wrap_intercept_channel(grpc.intercept_channel) grpc.aio.insecure_channel = _wrap_channel_async(grpc.aio.insecure_channel) grpc.aio.secure_channel = _wrap_channel_async(grpc.aio.secure_channel) diff --git a/sentry_sdk/integrations/grpc/client.py b/sentry_sdk/integrations/grpc/client.py index 9c5bec2faa..955c3c4217 100644 --- a/sentry_sdk/integrations/grpc/client.py +++ b/sentry_sdk/integrations/grpc/client.py @@ -19,6 +19,8 @@ class ClientInterceptor( grpc.UnaryUnaryClientInterceptor, grpc.UnaryStreamClientInterceptor # type: ignore ): + _is_intercepted = False + def intercept_unary_unary(self, continuation, client_call_details, request): # type: (ClientInterceptor, Callable[[ClientCallDetails, Message], _UnaryOutcome], ClientCallDetails, Message) -> _UnaryOutcome hub = Hub.current diff --git a/sentry_sdk/integrations/grpc/server.py b/sentry_sdk/integrations/grpc/server.py index 240e45f8ee..ce7c2f2a58 100644 --- a/sentry_sdk/integrations/grpc/server.py +++ b/sentry_sdk/integrations/grpc/server.py @@ -16,7 +16,7 @@ class ServerInterceptor(grpc.ServerInterceptor): # type: ignore - def __init__(self, find_name=None) -> None: + def __init__(self, find_name=None): # type: (ServerInterceptor, Optional[Callable[[ServicerContext], str]]) -> None self._find_method_name = find_name or ServerInterceptor._find_name diff --git a/tests/integrations/grpc/test_grpc.py b/tests/integrations/grpc/test_grpc.py index 014b59f41b..e6bd7c1ca3 100644 --- a/tests/integrations/grpc/test_grpc.py +++ b/tests/integrations/grpc/test_grpc.py @@ -186,7 +186,6 @@ def test_grpc_client_unary_stream_starts_span(sentry_init, capture_events_forksa assert span["data"] == { "type": "unary stream", "method": "/grpc_test_server.gRPCTestService/TestUnaryStream", - "code": "OK", } From 545f26b57bb4fd042b5af901e668c1fcac2f38ae Mon Sep 17 00:00:00 2001 From: Florian Dellekart Date: Mon, 9 Oct 2023 15:05:31 +0200 Subject: [PATCH 38/45] test(gRPC): Add tests for currently unsupported RPC types aio integration broke some request types, this is to verify this does not happen any more --- .../grpc/grpc_test_service_pb2.py | 4 +- .../grpc/grpc_test_service_pb2_grpc.py | 66 +++++++++++++++++++ .../grpc/protos/grpc_test_service.proto | 2 + tests/integrations/grpc/test_grpc.py | 35 +++++++++- tests/integrations/grpc/test_grpc_aio.py | 35 +++++++++- 5 files changed, 138 insertions(+), 4 deletions(-) diff --git a/tests/integrations/grpc/grpc_test_service_pb2.py b/tests/integrations/grpc/grpc_test_service_pb2.py index ba33818875..84ea7f632a 100644 --- a/tests/integrations/grpc/grpc_test_service_pb2.py +++ b/tests/integrations/grpc/grpc_test_service_pb2.py @@ -13,7 +13,7 @@ -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x17grpc_test_service.proto\x12\x10grpc_test_server\"\x1f\n\x0fgRPCTestMessage\x12\x0c\n\x04text\x18\x01 \x01(\t2\xbf\x01\n\x0fgRPCTestService\x12Q\n\tTestServe\x12!.grpc_test_server.gRPCTestMessage\x1a!.grpc_test_server.gRPCTestMessage\x12Y\n\x0fTestUnaryStream\x12!.grpc_test_server.gRPCTestMessage\x1a!.grpc_test_server.gRPCTestMessage0\x01\x62\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x17grpc_test_service.proto\x12\x10grpc_test_server\"\x1f\n\x0fgRPCTestMessage\x12\x0c\n\x04text\x18\x01 \x01(\t2\xf8\x02\n\x0fgRPCTestService\x12Q\n\tTestServe\x12!.grpc_test_server.gRPCTestMessage\x1a!.grpc_test_server.gRPCTestMessage\x12Y\n\x0fTestUnaryStream\x12!.grpc_test_server.gRPCTestMessage\x1a!.grpc_test_server.gRPCTestMessage0\x01\x12\\\n\x10TestStreamStream\x12!.grpc_test_server.gRPCTestMessage\x1a!.grpc_test_server.gRPCTestMessage(\x01\x30\x01\x12Y\n\x0fTestStreamUnary\x12!.grpc_test_server.gRPCTestMessage\x1a!.grpc_test_server.gRPCTestMessage(\x01\x62\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -23,5 +23,5 @@ _globals['_GRPCTESTMESSAGE']._serialized_start=45 _globals['_GRPCTESTMESSAGE']._serialized_end=76 _globals['_GRPCTESTSERVICE']._serialized_start=79 - _globals['_GRPCTESTSERVICE']._serialized_end=270 + _globals['_GRPCTESTSERVICE']._serialized_end=455 # @@protoc_insertion_point(module_scope) diff --git a/tests/integrations/grpc/grpc_test_service_pb2_grpc.py b/tests/integrations/grpc/grpc_test_service_pb2_grpc.py index 3f7a6098d3..ad897608ca 100644 --- a/tests/integrations/grpc/grpc_test_service_pb2_grpc.py +++ b/tests/integrations/grpc/grpc_test_service_pb2_grpc.py @@ -24,6 +24,16 @@ def __init__(self, channel): request_serializer=grpc__test__service__pb2.gRPCTestMessage.SerializeToString, response_deserializer=grpc__test__service__pb2.gRPCTestMessage.FromString, ) + self.TestStreamStream = channel.stream_stream( + '/grpc_test_server.gRPCTestService/TestStreamStream', + request_serializer=grpc__test__service__pb2.gRPCTestMessage.SerializeToString, + response_deserializer=grpc__test__service__pb2.gRPCTestMessage.FromString, + ) + self.TestStreamUnary = channel.stream_unary( + '/grpc_test_server.gRPCTestService/TestStreamUnary', + request_serializer=grpc__test__service__pb2.gRPCTestMessage.SerializeToString, + response_deserializer=grpc__test__service__pb2.gRPCTestMessage.FromString, + ) class gRPCTestServiceServicer(object): @@ -41,6 +51,18 @@ def TestUnaryStream(self, request, context): context.set_details('Method not implemented!') raise NotImplementedError('Method not implemented!') + def TestStreamStream(self, request_iterator, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def TestStreamUnary(self, request_iterator, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + def add_gRPCTestServiceServicer_to_server(servicer, server): rpc_method_handlers = { @@ -54,6 +76,16 @@ def add_gRPCTestServiceServicer_to_server(servicer, server): request_deserializer=grpc__test__service__pb2.gRPCTestMessage.FromString, response_serializer=grpc__test__service__pb2.gRPCTestMessage.SerializeToString, ), + 'TestStreamStream': grpc.stream_stream_rpc_method_handler( + servicer.TestStreamStream, + request_deserializer=grpc__test__service__pb2.gRPCTestMessage.FromString, + response_serializer=grpc__test__service__pb2.gRPCTestMessage.SerializeToString, + ), + 'TestStreamUnary': grpc.stream_unary_rpc_method_handler( + servicer.TestStreamUnary, + request_deserializer=grpc__test__service__pb2.gRPCTestMessage.FromString, + response_serializer=grpc__test__service__pb2.gRPCTestMessage.SerializeToString, + ), } generic_handler = grpc.method_handlers_generic_handler( 'grpc_test_server.gRPCTestService', rpc_method_handlers) @@ -97,3 +129,37 @@ def TestUnaryStream(request, grpc__test__service__pb2.gRPCTestMessage.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def TestStreamStream(request_iterator, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.stream_stream(request_iterator, target, '/grpc_test_server.gRPCTestService/TestStreamStream', + grpc__test__service__pb2.gRPCTestMessage.SerializeToString, + grpc__test__service__pb2.gRPCTestMessage.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def TestStreamUnary(request_iterator, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.stream_unary(request_iterator, target, '/grpc_test_server.gRPCTestService/TestStreamUnary', + grpc__test__service__pb2.gRPCTestMessage.SerializeToString, + grpc__test__service__pb2.gRPCTestMessage.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) diff --git a/tests/integrations/grpc/protos/grpc_test_service.proto b/tests/integrations/grpc/protos/grpc_test_service.proto index aafa75cf53..9eba747218 100644 --- a/tests/integrations/grpc/protos/grpc_test_service.proto +++ b/tests/integrations/grpc/protos/grpc_test_service.proto @@ -5,6 +5,8 @@ package grpc_test_server; service gRPCTestService{ rpc TestServe(gRPCTestMessage) returns (gRPCTestMessage); rpc TestUnaryStream(gRPCTestMessage) returns (stream gRPCTestMessage); + rpc TestStreamStream(stream gRPCTestMessage) returns (stream gRPCTestMessage); + rpc TestStreamUnary(stream gRPCTestMessage) returns (gRPCTestMessage); } message gRPCTestMessage { diff --git a/tests/integrations/grpc/test_grpc.py b/tests/integrations/grpc/test_grpc.py index e6bd7c1ca3..5fadddf63b 100644 --- a/tests/integrations/grpc/test_grpc.py +++ b/tests/integrations/grpc/test_grpc.py @@ -263,13 +263,36 @@ def test_grpc_client_and_servers_interceptors_integration( ) +@pytest.mark.forked +def test_stream_stream(sentry_init): + sentry_init(traces_sample_rate=1.0, integrations=[GRPCIntegration()]) + _set_up() + with grpc.insecure_channel(f"localhost:{PORT}") as channel: + stub = gRPCTestServiceStub(channel) + response_iterator = stub.TestStreamStream(iter((gRPCTestMessage(text="test"),))) + for response in response_iterator: + assert response.text == "test" + + +def test_stream_unary(sentry_init): + """Test to verify stream-stream works. + Tracing not supported for it yet. + """ + sentry_init(traces_sample_rate=1.0, integrations=[GRPCIntegration()]) + _set_up() + with grpc.insecure_channel(f"localhost:{PORT}") as channel: + stub = gRPCTestServiceStub(channel) + response = stub.TestStreamUnary(iter((gRPCTestMessage(text="test"),))) + assert response.text == "test" + + def _set_up(interceptors: Optional[List[grpc.ServerInterceptor]] = None): server = grpc.server( futures.ThreadPoolExecutor(max_workers=2), interceptors=interceptors, ) - add_gRPCTestServiceServicer_to_server(TestService, server) + add_gRPCTestServiceServicer_to_server(TestService(), server) server.add_insecure_port(f"[::]:{PORT}") server.start() @@ -299,3 +322,13 @@ def TestServe(request, context): # noqa: N802 def TestUnaryStream(request, context): # noqa: N802 for _ in range(3): yield gRPCTestMessage(text=request.text) + + @staticmethod + def TestStreamStream(request, context): + for r in request: + yield r + + @staticmethod + def TestStreamUnary(request, context): + requests = [r for r in request] + return requests.pop() diff --git a/tests/integrations/grpc/test_grpc_aio.py b/tests/integrations/grpc/test_grpc_aio.py index ed81ee0f5c..83a5f1d183 100644 --- a/tests/integrations/grpc/test_grpc_aio.py +++ b/tests/integrations/grpc/test_grpc_aio.py @@ -105,7 +105,7 @@ async def test_grpc_server_continues_transaction(capture_events, grpc_server): @pytest.mark.asyncio -async def test_grpc_server_exception(sentry_init, capture_events, grpc_server): +async def test_grpc_server_exception(capture_events, grpc_server): events = capture_events() async with grpc.aio.insecure_channel(f"localhost:{AIO_PORT}") as channel: @@ -181,6 +181,29 @@ async def test_grpc_client_unary_stream_starts_span( } +@pytest.mark.asyncio +async def test_stream_stream(grpc_server): + """Test to verify stream-stream works. + Tracing not supported for it yet. + """ + async with grpc.aio.insecure_channel(f"localhost:{AIO_PORT}") as channel: + stub = gRPCTestServiceStub(channel) + response = stub.TestStreamStream((gRPCTestMessage(text="test"),)) + async for r in response: + assert r.text == "test" + + +@pytest.mark.asyncio +async def test_stream_unary(grpc_server): + """Test to verify stream-stream works. + Tracing not supported for it yet. + """ + async with grpc.aio.insecure_channel(f"localhost:{AIO_PORT}") as channel: + stub = gRPCTestServiceStub(channel) + response = await stub.TestStreamUnary((gRPCTestMessage(text="test"),)) + assert response.text == "test" + + class TestService(gRPCTestServiceServicer): class TestException(Exception): def __init__(self): @@ -201,3 +224,13 @@ async def TestServe(cls, request, context): # noqa: N802 async def TestUnaryStream(cls, request, context): # noqa: N802 for _ in range(3): yield gRPCTestMessage(text=request.text) + + @classmethod + async def TestStreamStream(cls, request, context): + async for r in request: + yield r + + @classmethod + async def TestStreamUnary(cls, request, context): + requests = [r async for r in request] + return requests.pop() From dac5b602b830bf4ba5213705df6bc4ed91b8f153 Mon Sep 17 00:00:00 2001 From: Florian Dellekart Date: Mon, 9 Oct 2023 15:07:09 +0200 Subject: [PATCH 39/45] fix(gRPC): aio integration was breaking unsupported RPC types --- sentry_sdk/integrations/grpc/aio/server.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/sentry_sdk/integrations/grpc/aio/server.py b/sentry_sdk/integrations/grpc/aio/server.py index b06ecb4a30..a54545354d 100644 --- a/sentry_sdk/integrations/grpc/aio/server.py +++ b/sentry_sdk/integrations/grpc/aio/server.py @@ -67,6 +67,22 @@ async def wrapped(request, context): # type: ignore async for r in handler.unary_stream(request, context): yield r + elif handler.request_streaming and not handler.response_streaming: + handler_factory = grpc.stream_unary_rpc_method_handler + + async def wrapped(request, context): + # type: (Any, ServicerContext) -> Any + response = handler.stream_unary(request, context) + return await response + + elif handler.request_streaming and handler.response_streaming: + handler_factory = grpc.stream_stream_rpc_method_handler + + async def wrapped(request, context): # type: ignore + # type: (Any, ServicerContext) -> Any + async for r in handler.stream_stream(request, context): + yield r + return handler_factory( wrapped, request_deserializer=handler.request_deserializer, From 7cb78dfa3e62483492e3e30fe6727720472d032b Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 12 Oct 2023 09:11:55 +0200 Subject: [PATCH 40/45] Small typing fix --- sentry_sdk/integrations/grpc/aio/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/grpc/aio/client.py b/sentry_sdk/integrations/grpc/aio/client.py index e2f5e54e0a..4d9b8fcf73 100644 --- a/sentry_sdk/integrations/grpc/aio/client.py +++ b/sentry_sdk/integrations/grpc/aio/client.py @@ -70,7 +70,7 @@ async def intercept_unary_stream( continuation: Callable[[ClientCallDetails, Message], UnaryStreamCall], client_call_details: ClientCallDetails, request: Message, - ) -> AsyncIterable[Any] | UnaryStreamCall: + ) -> Union[AsyncIterable[Any], UnaryStreamCall]: hub = Hub.current method = client_call_details.method From b0e6d1964729da40cced7a0a96b3c9faa590e633 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 12 Oct 2023 09:13:23 +0200 Subject: [PATCH 41/45] Small linting fix --- tests/integrations/grpc/test_grpc.py | 4 ++-- tests/integrations/grpc/test_grpc_aio.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/integrations/grpc/test_grpc.py b/tests/integrations/grpc/test_grpc.py index 5fadddf63b..522b6fdf45 100644 --- a/tests/integrations/grpc/test_grpc.py +++ b/tests/integrations/grpc/test_grpc.py @@ -324,11 +324,11 @@ def TestUnaryStream(request, context): # noqa: N802 yield gRPCTestMessage(text=request.text) @staticmethod - def TestStreamStream(request, context): + def TestStreamStream(request, context): # noqa: N802 for r in request: yield r @staticmethod - def TestStreamUnary(request, context): + def TestStreamUnary(request, context): # noqa: N802 requests = [r for r in request] return requests.pop() diff --git a/tests/integrations/grpc/test_grpc_aio.py b/tests/integrations/grpc/test_grpc_aio.py index 83a5f1d183..23c8a191b2 100644 --- a/tests/integrations/grpc/test_grpc_aio.py +++ b/tests/integrations/grpc/test_grpc_aio.py @@ -226,11 +226,11 @@ async def TestUnaryStream(cls, request, context): # noqa: N802 yield gRPCTestMessage(text=request.text) @classmethod - async def TestStreamStream(cls, request, context): + async def TestStreamStream(cls, request, context): # noqa: N802 async for r in request: yield r @classmethod - async def TestStreamUnary(cls, request, context): + async def TestStreamUnary(cls, request, context): # noqa: N802 requests = [r async for r in request] return requests.pop() From afacd37959ba2d2b182a93acd1d11bbf6ca78772 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Mon, 6 Nov 2023 16:59:41 +0100 Subject: [PATCH 42/45] Consistent naming (was my fault I guess) --- sentry_sdk/integrations/grpc/aio/__init__.py | 2 +- sentry_sdk/integrations/grpc/aio/client.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/sentry_sdk/integrations/grpc/aio/__init__.py b/sentry_sdk/integrations/grpc/aio/__init__.py index 0687681166..59bfd502e5 100644 --- a/sentry_sdk/integrations/grpc/aio/__init__.py +++ b/sentry_sdk/integrations/grpc/aio/__init__.py @@ -1,2 +1,2 @@ from .server import ServerInterceptor # noqa: F401 -from .client import SentryClientInterceptor # noqa: F401 +from .client import ClientInterceptor # noqa: F401 diff --git a/sentry_sdk/integrations/grpc/aio/client.py b/sentry_sdk/integrations/grpc/aio/client.py index 4d9b8fcf73..e0b36541f3 100644 --- a/sentry_sdk/integrations/grpc/aio/client.py +++ b/sentry_sdk/integrations/grpc/aio/client.py @@ -13,7 +13,7 @@ from sentry_sdk.consts import OP -class SentryClientInterceptor: +class ClientInterceptor: @staticmethod def _update_client_call_details_metadata_from_hub( client_call_details: ClientCallDetails, hub: Hub @@ -35,7 +35,7 @@ def _update_client_call_details_metadata_from_hub( return client_call_details -class SentryUnaryUnaryClientInterceptor(SentryClientInterceptor, UnaryUnaryClientInterceptor): # type: ignore +class SentryUnaryUnaryClientInterceptor(ClientInterceptor, UnaryUnaryClientInterceptor): # type: ignore async def intercept_unary_unary( self, continuation: Callable[[ClientCallDetails, Message], UnaryUnaryCall], @@ -63,7 +63,7 @@ async def intercept_unary_unary( class SentryUnaryStreamClientInterceptor( - SentryClientInterceptor, UnaryStreamClientInterceptor # type: ignore + ClientInterceptor, UnaryStreamClientInterceptor # type: ignore ): async def intercept_unary_stream( self, From 7be1306ec3891aa8714cb90f0ac7a7748c7afd39 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Tue, 7 Nov 2023 11:36:56 +0100 Subject: [PATCH 43/45] Set nicer transaction name for async server interceptor --- sentry_sdk/integrations/grpc/__init__.py | 4 +--- sentry_sdk/integrations/grpc/aio/server.py | 9 ++++----- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/sentry_sdk/integrations/grpc/__init__.py b/sentry_sdk/integrations/grpc/__init__.py index 111138ab07..2cb7c8192a 100644 --- a/sentry_sdk/integrations/grpc/__init__.py +++ b/sentry_sdk/integrations/grpc/__init__.py @@ -127,9 +127,7 @@ def patched_aio_server( interceptors: Optional[Sequence[grpc.ServerInterceptor]] = None, **kwargs: P.kwargs, ) -> Server: - server_interceptor = AsyncServerInterceptor( - find_name=lambda request: request.__class__ - ) + server_interceptor = AsyncServerInterceptor() interceptors = [server_interceptor, *(interceptors or [])] return func(*args, interceptors=interceptors, **kwargs) # type: ignore diff --git a/sentry_sdk/integrations/grpc/aio/server.py b/sentry_sdk/integrations/grpc/aio/server.py index a54545354d..73acb7e711 100644 --- a/sentry_sdk/integrations/grpc/aio/server.py +++ b/sentry_sdk/integrations/grpc/aio/server.py @@ -21,12 +21,13 @@ class ServerInterceptor(grpc.aio.ServerInterceptor): # type: ignore def __init__(self, find_name=None): # type: (ServerInterceptor, Callable[[ServicerContext], str] | None) -> None - self._find_method_name = find_name or ServerInterceptor._find_name + self._find_method_name = find_name or self._find_name super(ServerInterceptor, self).__init__() async def intercept_service(self, continuation, handler_call_details): # type: (ServerInterceptor, Callable[[HandlerCallDetails], Awaitable[RpcMethodHandler]], HandlerCallDetails) -> Awaitable[RpcMethodHandler] + self._handler_call_details = handler_call_details handler = await continuation(handler_call_details) if not handler.request_streaming and not handler.response_streaming: @@ -89,7 +90,5 @@ async def wrapped(request, context): # type: ignore response_serializer=handler.response_serializer, ) - @staticmethod - def _find_name(context): - # type: (ServicerContext) -> str - return context._rpc_event.call_details.method.decode() + def _find_name(self, context): + return self._handler_call_details.method From f83e610e1487a708797ce0d69920a44250392b62 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Wed, 8 Nov 2023 15:44:03 +0100 Subject: [PATCH 44/45] Make linter happy --- tests/integrations/grpc/test_grpc.py | 16 ++++++++-------- tests/integrations/grpc/test_grpc_aio.py | 16 ++++++++-------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/tests/integrations/grpc/test_grpc.py b/tests/integrations/grpc/test_grpc.py index 16fe1cbc8b..0813d655ae 100644 --- a/tests/integrations/grpc/test_grpc.py +++ b/tests/integrations/grpc/test_grpc.py @@ -60,7 +60,7 @@ def test_grpc_server_other_interceptors(sentry_init, capture_events_forksafe): server = _set_up(interceptors=[mock_interceptor]) - with grpc.insecure_channel(f"localhost:{PORT}") as channel: + with grpc.insecure_channel("localhost:{}".format(PORT)) as channel: stub = gRPCTestServiceStub(channel) stub.TestServe(gRPCTestMessage(text="test")) @@ -132,7 +132,7 @@ def test_grpc_client_starts_span(sentry_init, capture_events_forksafe): server = _set_up() - with grpc.insecure_channel(f"localhost:{PORT}") as channel: + with grpc.insecure_channel("localhost:{}".format(PORT)) as channel: stub = gRPCTestServiceStub(channel) with start_transaction(): @@ -165,7 +165,7 @@ def test_grpc_client_unary_stream_starts_span(sentry_init, capture_events_forksa server = _set_up() - with grpc.insecure_channel(f"localhost:{PORT}") as channel: + with grpc.insecure_channel("localhost:{}".format(PORT)) as channel: stub = gRPCTestServiceStub(channel) with start_transaction(): @@ -207,7 +207,7 @@ def test_grpc_client_other_interceptor(sentry_init, capture_events_forksafe): server = _set_up() - with grpc.insecure_channel(f"localhost:{PORT}") as channel: + with grpc.insecure_channel("localhost:{}".format(PORT)) as channel: channel = grpc.intercept_channel(channel, MockClientInterceptor()) stub = gRPCTestServiceStub(channel) @@ -245,7 +245,7 @@ def test_grpc_client_and_servers_interceptors_integration( server = _set_up() - with grpc.insecure_channel(f"localhost:{PORT}") as channel: + with grpc.insecure_channel("localhost:{}".format(PORT)) as channel: stub = gRPCTestServiceStub(channel) with start_transaction(): @@ -267,7 +267,7 @@ def test_grpc_client_and_servers_interceptors_integration( def test_stream_stream(sentry_init): sentry_init(traces_sample_rate=1.0, integrations=[GRPCIntegration()]) _set_up() - with grpc.insecure_channel(f"localhost:{PORT}") as channel: + with grpc.insecure_channel("localhost:{}".format(PORT)) as channel: stub = gRPCTestServiceStub(channel) response_iterator = stub.TestStreamStream(iter((gRPCTestMessage(text="test"),))) for response in response_iterator: @@ -280,7 +280,7 @@ def test_stream_unary(sentry_init): """ sentry_init(traces_sample_rate=1.0, integrations=[GRPCIntegration()]) _set_up() - with grpc.insecure_channel(f"localhost:{PORT}") as channel: + with grpc.insecure_channel("localhost:{}".format(PORT)) as channel: stub = gRPCTestServiceStub(channel) response = stub.TestStreamUnary(iter((gRPCTestMessage(text="test"),))) assert response.text == "test" @@ -293,7 +293,7 @@ def _set_up(interceptors: Optional[List[grpc.ServerInterceptor]] = None): ) add_gRPCTestServiceServicer_to_server(TestService(), server) - server.add_insecure_port(f"[::]:{PORT}") + server.add_insecure_port("[::]:{}".format(PORT)) server.start() return server diff --git a/tests/integrations/grpc/test_grpc_aio.py b/tests/integrations/grpc/test_grpc_aio.py index 23c8a191b2..d5a716bb4b 100644 --- a/tests/integrations/grpc/test_grpc_aio.py +++ b/tests/integrations/grpc/test_grpc_aio.py @@ -34,7 +34,7 @@ def event_loop(request): async def grpc_server(sentry_init, event_loop): sentry_init(traces_sample_rate=1.0, integrations=[GRPCIntegration()]) server = grpc.aio.server() - server.add_insecure_port(f"[::]:{AIO_PORT}") + server.add_insecure_port("[::]:{}".format(AIO_PORT)) add_gRPCTestServiceServicer_to_server(TestService, server) await event_loop.create_task(server.start()) @@ -49,7 +49,7 @@ async def grpc_server(sentry_init, event_loop): async def test_grpc_server_starts_transaction(capture_events, grpc_server): events = capture_events() - async with grpc.aio.insecure_channel(f"localhost:{AIO_PORT}") as channel: + async with grpc.aio.insecure_channel("localhost:{}".format(AIO_PORT)) as channel: stub = gRPCTestServiceStub(channel) await stub.TestServe(gRPCTestMessage(text="test")) @@ -68,7 +68,7 @@ async def test_grpc_server_starts_transaction(capture_events, grpc_server): async def test_grpc_server_continues_transaction(capture_events, grpc_server): events = capture_events() - async with grpc.aio.insecure_channel(f"localhost:{AIO_PORT}") as channel: + async with grpc.aio.insecure_channel("localhost:{}".format(AIO_PORT)) as channel: stub = gRPCTestServiceStub(channel) with sentry_sdk.start_transaction() as transaction: @@ -108,7 +108,7 @@ async def test_grpc_server_continues_transaction(capture_events, grpc_server): async def test_grpc_server_exception(capture_events, grpc_server): events = capture_events() - async with grpc.aio.insecure_channel(f"localhost:{AIO_PORT}") as channel: + async with grpc.aio.insecure_channel("localhost:{}".format(AIO_PORT)) as channel: stub = gRPCTestServiceStub(channel) try: await stub.TestServe(gRPCTestMessage(text="exception")) @@ -130,7 +130,7 @@ async def test_grpc_client_starts_span( ): events = capture_events_forksafe() - async with grpc.aio.insecure_channel(f"localhost:{AIO_PORT}") as channel: + async with grpc.aio.insecure_channel("localhost:{}".format(AIO_PORT)) as channel: stub = gRPCTestServiceStub(channel) with start_transaction(): await stub.TestServe(gRPCTestMessage(text="test")) @@ -159,7 +159,7 @@ async def test_grpc_client_unary_stream_starts_span( ): events = capture_events_forksafe() - async with grpc.aio.insecure_channel(f"localhost:{AIO_PORT}") as channel: + async with grpc.aio.insecure_channel("localhost:{}".format(AIO_PORT)) as channel: stub = gRPCTestServiceStub(channel) with start_transaction(): response = stub.TestUnaryStream(gRPCTestMessage(text="test")) @@ -186,7 +186,7 @@ async def test_stream_stream(grpc_server): """Test to verify stream-stream works. Tracing not supported for it yet. """ - async with grpc.aio.insecure_channel(f"localhost:{AIO_PORT}") as channel: + async with grpc.aio.insecure_channel("localhost:{}".format(AIO_PORT)) as channel: stub = gRPCTestServiceStub(channel) response = stub.TestStreamStream((gRPCTestMessage(text="test"),)) async for r in response: @@ -198,7 +198,7 @@ async def test_stream_unary(grpc_server): """Test to verify stream-stream works. Tracing not supported for it yet. """ - async with grpc.aio.insecure_channel(f"localhost:{AIO_PORT}") as channel: + async with grpc.aio.insecure_channel("localhost:{}".format(AIO_PORT)) as channel: stub = gRPCTestServiceStub(channel) response = await stub.TestStreamUnary((gRPCTestMessage(text="test"),)) assert response.text == "test" From 12714598e98b35d7535e5be17c080d493ab3f4a8 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Wed, 8 Nov 2023 16:00:30 +0100 Subject: [PATCH 45/45] Make mypy happy --- sentry_sdk/integrations/grpc/aio/server.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sentry_sdk/integrations/grpc/aio/server.py b/sentry_sdk/integrations/grpc/aio/server.py index 73acb7e711..56d21a90a1 100644 --- a/sentry_sdk/integrations/grpc/aio/server.py +++ b/sentry_sdk/integrations/grpc/aio/server.py @@ -91,4 +91,5 @@ async def wrapped(request, context): # type: ignore ) def _find_name(self, context): + # type: (ServicerContext) -> str return self._handler_call_details.method