Repository: pinpoint-apm/pinpoint-c-agent (language: python)

Issue:
Can PINPOINT C -agent support GRPC
I saw something that said GRPC was supported, tried to find the sample code but couldn't find it。


Discussion / hints:
pinpoint collector gRpc protocol ?
I set up a GRPC service with Python, I want to introduce pinpoint monitoring. Sample code for Flask and FastAPI is found on the official website, but not for GRPC. Therefore, I want to confirm whether the GRPC service is supported. If so, is there an example
> I set up a GRPC service with Python, I want to introduce pinpoint monitoring. Sample code for Flask and FastAPI is found on the official website, but not for GRPC. Therefore, I want to confirm whether the GRPC service is supported. If so, is there an example

Thank your suggestion, We will check python-gRpc implementation whether supports setting interceptor or  not. 
Hope add this feature too
Hi, I also need it. 
Can you share what your current progress is? @eeliu 
Thank your suggestion, I will try to add it in schedule of Q3/Q4. 

 > sorry 
This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.
This issue was closed because it has been stalled for 5 days with no activity.

Fix patch:
diff --git a/plugins/PY/pinpointPy/grpc_/CHANGES.md b/plugins/PY/pinpointPy/grpc_/CHANGES.md
new file mode 100644
index 00000000..4cb152be
--- /dev/null
+++ b/plugins/PY/pinpointPy/grpc_/CHANGES.md
@@ -0,0 +1,4 @@
+## versions
+
+#### v0.0.1 3-25-2024
+- support thread mode of grpc
\ No newline at end of file
diff --git a/plugins/PY/pinpointPy/grpc_/GrpcRequestPlugins.py b/plugins/PY/pinpointPy/grpc_/GrpcRequestPlugins.py
new file mode 100644
index 00000000..6f0bd7c2
--- /dev/null
+++ b/plugins/PY/pinpointPy/grpc_/GrpcRequestPlugins.py
@@ -0,0 +1,81 @@
+# ------------------------------------------------------------------------------
+#  Copyright  2020. NAVER Corp.                                                -
+#                                                                              -
+#  Licensed under the Apache License, Version 2.0 (the "License");             -
+#  you may not use this file except in compliance with the License.            -
+#  You may obtain a copy of the License at                                     -
+#                                                                              -
+#   http://www.apache.org/licenses/LICENSE-2.0                                 -
+#                                                                              -
+#  Unless required by applicable law or agreed to in writing, software         -
+#  distributed under the License is distributed on an "AS IS" BASIS,           -
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.    -
+#  See the License for the specific language governing permissions and         -
+#  limitations under the License.                                              -
+# ------------------------------------------------------------------------------
+# Created by eeliu at 3/25/24
+import grpc
+from grpc_interceptor import ServerInterceptor
+from typing import Callable, Any
+from pinpointPy import Common, pinpoint, Defines
+
+
+class GrpcRequestPlugins(Common.PinTrace):
+    def __init__(self, name):
+        super().__init__(name)
+
+    @staticmethod
+    def isSample(*args, **kwargs):
+        return True, 0, args, kwargs
+
+    def onBefore(self, parentId, *args, **kwargs):
+        trace_id, args, kwargs = super().onBefore(parentId, *args, **kwargs)
+        pinpoint.add_trace_header(
+            Defines.PP_APP_NAME, pinpoint.app_name(), trace_id)
+        pinpoint.add_trace_header(
+            Defines.PP_APP_ID, pinpoint.app_id(), trace_id)
+        pinpoint.add_context(Defines.PP_APP_NAME,
+                             pinpoint.app_name(), trace_id)
+        pinpoint.add_trace_header(
+            Defines.PP_INTERCEPTOR_NAME, self.getUniqueName(), trace_id)
+        context = args[3]
+        method = args[4]
+        pinpoint.add_trace_header(
+            Defines.PP_REQ_CLIENT, context.peer(), trace_id, trace_id)
+        pinpoint.add_trace_header(Defines.PP_REQ_URI, method, trace_id)
+        pinpoint.add_trace_header(
+            Defines.PP_REQ_SERVER, "not_support", trace_id)
+        pinpoint.add_trace_header(
+            Defines.PP_SERVER_TYPE, Defines.PYTHON, trace_id)
+        pinpoint.add_context(Defines.PP_SERVER_TYPE, Defines.PYTHON, trace_id)
+        self.sid = pinpoint.gen_sid()
+        pinpoint.add_context(Defines.PP_SPAN_ID, self.sid, trace_id)
+        self.tid = pinpoint.gen_tid()
+        pinpoint.add_context(Defines.PP_TRANSCATION_ID, self.tid, trace_id)
+        pinpoint.add_trace_header(
+            Defines.PP_TRANSCATION_ID, self.tid, trace_id)
+        pinpoint.add_trace_header(Defines.PP_SPAN_ID, self.sid, trace_id)
+        pinpoint.add_context(Defines.PP_TRANSCATION_ID, self.tid, trace_id)
+        pinpoint.add_context(Defines.PP_SPAN_ID, self.sid, trace_id)
+        return trace_id, args, kwargs
+
+    def onEnd(self, trace_id, ret):
+        super().onEnd(trace_id, ret)
+        return ret
+
+    def onException(self, trace_id, e):
+        pinpoint.mark_as_error(str(e), "", trace_id)
+        raise e
+
+
+class PinpointServerInterceptor(ServerInterceptor):
+    @GrpcRequestPlugins("grpc.intercept")
+    def intercept(
+        self,
+        method: Callable,
+        request_or_iterator: Any,
+        context: grpc.ServicerContext,
+        method_name: str,
+    ) -> Any:
+        ret = super().intercept(method, request_or_iterator, context, method_name)
+        return ret
diff --git a/plugins/PY/pinpointPy/grpc_/__init__.py b/plugins/PY/pinpointPy/grpc_/__init__.py
new file mode 100644
index 00000000..2afd9ae4
--- /dev/null
+++ b/plugins/PY/pinpointPy/grpc_/__init__.py
@@ -0,0 +1,30 @@
+# ------------------------------------------------------------------------------
+#  Copyright  2024. NAVER Corp.                                                -
+#                                                                              -
+#  Licensed under the Apache License, Version 2.0 (the "License");             -
+#  you may not use this file except in compliance with the License.            -
+#  You may obtain a copy of the License at                                     -
+#                                                                              -
+#   http://www.apache.org/licenses/LICENSE-2.0                                 -
+#                                                                              -
+#  Unless required by applicable law or agreed to in writing, software         -
+#  distributed under the License is distributed on an "AS IS" BASIS,           -
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.    -
+#  See the License for the specific language governing permissions and         -
+#  limitations under the License.                                              -
+# ------------------------------------------------------------------------------
+
+try:
+    import grpc_interceptor
+except:
+    raise ModuleNotFoundError(
+        '''
+            pinpointPy.grpc_ needs grpc_interceptor, try to install by "pip install grpc-interceptor".
+            read more: `https://github.com/d5h-foss/grpc-interceptor` and `https://github.com/pinpoint-apm/pinpoint-c-agent`
+        ''')
+
+
+from pinpointPy.grpc_.GrpcRequestPlugins import PinpointServerInterceptor
+
+__version__ = '0.0.1'
+__all__ = ['PinpointServerInterceptor']
diff --git a/plugins/PY/requirements.txt b/plugins/PY/requirements.txt
index 033b9b88..1581d697 100644
--- a/plugins/PY/requirements.txt
+++ b/plugins/PY/requirements.txt
@@ -9,4 +9,6 @@ redis==5.0.1
 requests==2.31.0
 SQLAlchemy==2.0.23
 starlette-context==0.3.6
-mysqlclient==2.2.0
\ No newline at end of file
+mysqlclient==2.2.0
+grpcio
+grpc-interceptor==0.15.4
\ No newline at end of file


Test patch:
diff --git a/setup_pypi_test.py b/setup_pypi_test.py
index ac42177b..fdc50a82 100644
--- a/setup_pypi_test.py
+++ b/setup_pypi_test.py
@@ -37,7 +37,7 @@
                  Path(cwd, './common/src')]
 
 setup(name='pinpointPy',
-      version="1.2.5",  # don't forget update __version__ in pinpointPy/__init__.py
+      version="1.3.0",  # don't forget update __version__ in pinpointPy/__init__.py
       author="cd_pinpoint members",
       author_email='dl_cd_pinpoint@navercorp.com',
       license='Apache License 2.0',
diff --git a/testapps/PY/app.py b/testapps/PY/app.py
index 2d1a99da..310b83ff 100644
--- a/testapps/PY/app.py
+++ b/testapps/PY/app.py
@@ -57,7 +57,7 @@
 
 app = Flask(__name__)
 
-set_agent("cd.dev.test.py", "cd.dev.test.py",
+set_agent("cd.dev.test.flask", "cd.dev.test.py",
           'tcp:dev-collector:10000', -1, 0, logging.DEBUG)
 
 app.wsgi_app = PinPointMiddleWare(app, app.wsgi_app)
diff --git a/testapps/backend/app.py b/testapps/backend/app.py
index fc648e25..3478addc 100644
--- a/testapps/backend/app.py
+++ b/testapps/backend/app.py
@@ -57,7 +57,7 @@
 
 app = Flask(__name__)
 
-set_agent("cd.dev.test.py.backend", "cd.dev.test.py.backend",
+set_agent("cd.dev.test.backend", "cd.dev.test.py.backend",
           'tcp:dev-collector:10000', -1, 0, logging.DEBUG)
 
 app.wsgi_app = PinPointMiddleWare(app, app.wsgi_app)
diff --git a/testapps/compose.yaml b/testapps/compose.yaml
index d6b2433b..011ee831 100644
--- a/testapps/compose.yaml
+++ b/testapps/compose.yaml
@@ -4,27 +4,41 @@ services:
     restart: always
     environment:
       MYSQL_ROOT_PASSWORD: 'password'
-      MYSQL_TCP_PORT: 3306  
+      MYSQL_TCP_PORT: 3306
     ports:
-      - '33060:3306' 
+      - '33060:3306'
     volumes:
       - ..:/workspace:cached
-       
+
     healthcheck:
-      test: ["CMD", "mysqladmin","-uroot","-ppassword" ,"ping", "-h", "localhost"]
+      test:
+        [
+          "CMD",
+          "mysqladmin",
+          "-uroot",
+          "-ppassword",
+          "ping",
+          "-h",
+          "localhost"
+        ]
       interval: 15s
       timeout: 10s
       retries: 10
-  
+
   dev-mysql-setup:
-    image: mysql:5.7 
+    image: mysql:5.7
     depends_on:
       dev-mysql:
         condition: service_healthy
     volumes:
-      - ..:/workspace:cached    
+      - ..:/workspace:cached
     # todo load all data 
-    entrypoint: [ "bash", "-c", "cd /workspace/testapps/ && sh load_sample_db.sh"]
+    entrypoint:
+      [
+        "bash",
+        "-c",
+        "cd /workspace/testapps/ && sh load_sample_db.sh"
+      ]
 
   redis:
     image: redis:alpine
@@ -57,8 +71,8 @@ services:
 
     healthcheck:
       test: curl -f http://testapp-flask
-      interval: 5s 
-      timeout: 1s       
+      interval: 5s
+      timeout: 1s
 
   testapp-php:
     container_name: yii2
@@ -66,7 +80,7 @@ services:
       redis:
         condition: service_healthy
       dev-collector:
-        condition: service_started        
+        condition: service_started
     restart: always
     build:
       dockerfile: testapps/php.dockerfile
@@ -76,17 +90,17 @@ services:
 
     healthcheck:
       test: curl -f http://testapp-php
-      interval: 5s 
-      timeout: 1s       
+      interval: 5s
+      timeout: 1s
 
   testapp-fastapi:
     container_name: fastapi
     depends_on:
       redis:
-        condition: service_healthy  
+        condition: service_healthy
       dev-collector:
         condition: service_started
-        
+
     restart: always
     build:
       dockerfile: testapps/fastapi.dockerfile
@@ -95,8 +109,8 @@ services:
       - 8186:8000
     healthcheck:
       test: curl -f http://testapp-fastapi:8000
-      interval: 5s 
-      timeout: 1s        
+      interval: 5s
+      timeout: 1s
 
   # test call self
   backend:
@@ -104,33 +118,33 @@ services:
     container_name: backend
     depends_on:
       redis:
-        condition: service_healthy    
+        condition: service_healthy
     build:
       dockerfile: testapps/backend.dockerfile
       context: ../
     ports:
-      - 8187:80  
+      - 8187:80
 
     healthcheck:
       test: curl -f http://backend
-      interval: 5s 
-      timeout: 1s       
-  
+      interval: 5s
+      timeout: 1s
+
   testapp-django:
     container_name: django
     depends_on:
       redis:
-        condition: service_healthy    
+        condition: service_healthy
     restart: always
     build:
       dockerfile: testapps/django.dockerfile
       context: ../
     ports:
-      - 8188:8000  
+      - 8188:8000
     healthcheck:
       test: curl -f http://testapp-django:8000/polls
-      interval: 5s 
-      timeout: 1s     
+      interval: 5s
+      timeout: 1s
 
   httpbin:
     build:
@@ -142,7 +156,7 @@ services:
       interval: 5s
       timeout: 10s
       retries: 50
-    
+
   dev-collector:
     restart: always
     build:
@@ -164,20 +178,19 @@ services:
     volumes:
       - /etc/hosts:/etc/hosts
 
-
   mongodb:
     image: 'mongo'
     restart: always
     hostname: mongodb
     healthcheck:
       test: echo 'db.runCommand("ping").ok' | mongosh localhost:27017/test --quiet
-    # ports:
+      # ports:
       # - 27017:27017
 
   python-plugins:
     build:
       dockerfile: testapps/python-plugins.dockerfile
-      context: ../     
+      context: ../
     profiles:
       - testing
     depends_on:
@@ -186,4 +199,32 @@ services:
       httpbin:
         condition: service_healthy
       mongodb:
-        condition: service_healthy
\ No newline at end of file
+        condition: service_healthy
+
+  grpc-py:
+    container_name: grpc-py
+    restart: always
+    build:
+      dockerfile: testapps/grpc_py.dockerfile
+      context: ../
+    ports:
+      - 50050:50050
+    depends_on:
+      dev-collector:
+        condition: service_started
+      redis:
+        condition: service_healthy
+
+  grpc-py-client:
+    container_name: grpc-py-client
+    depends_on:
+      grpc-py:
+        condition: service_started
+    restart: always
+    build:
+      dockerfile: testapps/grpc_py_client.dockerfile
+      context: ../
+    # healthcheck:
+    #   test: python /app/route_guide_client.py
+    #   interval: 5s
+    #   timeout: 1s
diff --git a/testapps/django/mysite/mysite/settings.py b/testapps/django/mysite/mysite/settings.py
index d03e6608..33fa2ee0 100644
--- a/testapps/django/mysite/mysite/settings.py
+++ b/testapps/django/mysite/mysite/settings.py
@@ -45,7 +45,7 @@
 
 use_thread_local_context()
 monkey_patch_for_pinpoint()
-set_agent("cd.dev.test.py", "cd.dev.test.py",
+set_agent("cd.dev.test.django", "cd.dev.test.py",
           'tcp:dev-collector:10000', -1, 0, logging.DEBUG)
 
 MIDDLEWARE = [
diff --git a/testapps/fastapi/main.py b/testapps/fastapi/main.py
index d6cc8161..f2b94ad5 100644
--- a/testapps/fastapi/main.py
+++ b/testapps/fastapi/main.py
@@ -48,7 +48,7 @@ async def dispatch(self, request: Request, call_next):
 ]
 
 
-set_agent("cd.dev.test.py", "cd.dev.test.py",
+set_agent("cd.dev.test.fastapi", "cd.dev.test.py",
           'tcp:dev-collector:10000', -1, 0, logging.DEBUG)
 
 use_starlette_context()
diff --git a/testapps/grpc-py/asyncio_route_guide_client.py b/testapps/grpc-py/asyncio_route_guide_client.py
new file mode 100644
index 00000000..c874cc53
--- /dev/null
+++ b/testapps/grpc-py/asyncio_route_guide_client.py
@@ -0,0 +1,143 @@
+# Copyright 2020 The gRPC Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""The Python AsyncIO implementation of the gRPC route guide client."""
+
+import asyncio
+import logging
+import random
+from typing import Iterable, List
+
+import grpc
+import route_guide_pb2
+import route_guide_pb2_grpc
+import route_guide_resources
+
+
+def make_route_note(
+    message: str, latitude: int, longitude: int
+) -> route_guide_pb2.RouteNote:
+    return route_guide_pb2.RouteNote(
+        message=message,
+        location=route_guide_pb2.Point(latitude=latitude, longitude=longitude),
+    )
+
+
+# Performs an unary call
+async def guide_get_one_feature(
+    stub: route_guide_pb2_grpc.RouteGuideStub, point: route_guide_pb2.Point
+) -> None:
+    feature = await stub.GetFeature(point)
+    if not feature.location:
+        print("Server returned incomplete feature")
+        return
+
+    if feature.name:
+        print(f"Feature called {feature.name} at {feature.location}")
+    else:
+        print(f"Found no feature at {feature.location}")
+
+
+async def guide_get_feature(stub: route_guide_pb2_grpc.RouteGuideStub) -> None:
+    # The following two coroutines will be wrapped in a Future object
+    # and scheduled in the event loop so that they can run concurrently
+    task_group = asyncio.gather(
+        guide_get_one_feature(
+            stub,
+            route_guide_pb2.Point(latitude=409146138, longitude=-746188906),
+        ),
+        guide_get_one_feature(
+            stub, route_guide_pb2.Point(latitude=0, longitude=0)
+        ),
+    )
+    # Wait until the Future is resolved
+    await task_group
+
+
+# Performs a server-streaming call
+async def guide_list_features(
+    stub: route_guide_pb2_grpc.RouteGuideStub,
+) -> None:
+    rectangle = route_guide_pb2.Rectangle(
+        lo=route_guide_pb2.Point(latitude=400000000, longitude=-750000000),
+        hi=route_guide_pb2.Point(latitude=420000000, longitude=-730000000),
+    )
+    print("Looking for features between 40, -75 and 42, -73")
+
+    features = stub.ListFeatures(rectangle)
+
+    async for feature in features:
+        print(f"Feature called {feature.name} at {feature.location}")
+
+
+def generate_route(
+    feature_list: List[route_guide_pb2.Feature],
+) -> Iterable[route_guide_pb2.Point]:
+    for _ in range(0, 10):
+        random_feature = random.choice(feature_list)
+        print(f"Visiting point {random_feature.location}")
+        yield random_feature.location
+
+
+# Performs a client-streaming call
+async def guide_record_route(stub: route_guide_pb2_grpc.RouteGuideStub) -> None:
+    feature_list = route_guide_resources.read_route_guide_database()
+    route_iterator = generate_route(feature_list)
+
+    # gRPC AsyncIO client-streaming RPC API accepts both synchronous iterables
+    # and async iterables.
+    route_summary = await stub.RecordRoute(route_iterator)
+    print(f"Finished trip with {route_summary.point_count} points")
+    print(f"Passed {route_summary.feature_count} features")
+    print(f"Travelled {route_summary.distance} meters")
+    print(f"It took {route_summary.elapsed_time} seconds")
+
+
+def generate_messages() -> Iterable[route_guide_pb2.RouteNote]:
+    messages = [
+        make_route_note("First message", 0, 0),
+        make_route_note("Second message", 0, 1),
+        make_route_note("Third message", 1, 0),
+        make_route_note("Fourth message", 0, 0),
+        make_route_note("Fifth message", 1, 0),
+    ]
+    for msg in messages:
+        print(f"Sending {msg.message} at {msg.location}")
+        yield msg
+
+
+# Performs a bidi-streaming call
+async def guide_route_chat(stub: route_guide_pb2_grpc.RouteGuideStub) -> None:
+    # gRPC AsyncIO bidi-streaming RPC API accepts both synchronous iterables
+    # and async iterables.
+    call = stub.RouteChat(generate_messages())
+    async for response in call:
+        print(f"Received message {response.message} at {response.location}")
+
+
+async def main() -> None:
+    async with grpc.aio.insecure_channel("localhost:50051") as channel:
+        stub = route_guide_pb2_grpc.RouteGuideStub(channel)
+        print("-------------- GetFeature --------------")
+        await guide_get_feature(stub)
+        print("-------------- ListFeatures --------------")
+        await guide_list_features(stub)
+        print("-------------- RecordRoute --------------")
+        await guide_record_route(stub)
+        print("-------------- RouteChat --------------")
+        await guide_route_chat(stub)
+
+
+if __name__ == "__main__":
+    logging.basicConfig(level=logging.INFO)
+    asyncio.get_event_loop().run_until_complete(main())
diff --git a/testapps/grpc-py/asyncio_route_guide_server.py b/testapps/grpc-py/asyncio_route_guide_server.py
new file mode 100644
index 00000000..578e5bcc
--- /dev/null
+++ b/testapps/grpc-py/asyncio_route_guide_server.py
@@ -0,0 +1,198 @@
+# Copyright 2020 The gRPC Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""The Python AsyncIO implementation of the gRPC route guide server."""
+
+import asyncio
+import logging
+import math
+import time
+from typing import AsyncIterable, Iterable
+
+import grpc
+import route_guide_pb2
+import route_guide_pb2_grpc
+import route_guide_resources
+
+
+from typing import Callable, Any
+from grpc_interceptor import AsyncServerInterceptor
+from grpc_interceptor.exceptions import GrpcException
+
+
+class ExceptionToStatusInterceptor(AsyncServerInterceptor):
+    def intercept(
+        self,
+        method: Callable,
+        request_or_iterator: Any,
+        context: grpc.ServicerContext,
+        method_name: str,
+    ) -> Any:
+        """Override this method to implement a custom interceptor.
+         You should call method(request_or_iterator, context) to invoke the
+         next handler (either the RPC method implementation, or the
+         next interceptor in the list).
+         Args:
+             method: The next interceptor, or method implementation.
+             request_or_iterator: The RPC request, as a protobuf message.
+             context: The ServicerContext pass by gRPC to the service.
+             method_name: A string of the form
+                 "/protobuf.package.Service/Method"
+         Returns:
+             This should generally return the result of
+             method(request_or_iterator, context), which is typically the RPC
+             method response, as a protobuf message. The interceptor
+             is free to modify this in some way, however.
+         """
+        try:
+            print(f'{method_name} {context}')
+            ret = super().intercept(method, request_or_iterator, context, method_name)
+            print(f'{ret} \n')
+            return ret
+            # return await  method(request_or_iterator, context)
+            # response_or_iterator = await method(request_or_iterator, context)
+            # if hasattr(response_or_iterator, "__aiter__"):
+            #     print(f'{response_or_iterator} __aiter__ \n ')
+            #     return response_or_iterator
+            # else:
+            #     print(f'{response_or_iterator} {method} \n ')
+            #     return  response_or_iterator
+        except GrpcException as e:
+            context.set_code(e.status_code)
+            context.set_details(e.details)
+            raise
+
+
+interceptors = [ExceptionToStatusInterceptor()]
+
+
+def get_feature(
+    feature_db: Iterable[route_guide_pb2.Feature], point: route_guide_pb2.Point
+) -> route_guide_pb2.Feature:
+    """Returns Feature at given location or None."""
+    for feature in feature_db:
+        if feature.location == point:
+            return feature
+    return None
+
+
+def get_distance(
+    start: route_guide_pb2.Point, end: route_guide_pb2.Point
+) -> float:
+    """Distance between two points."""
+    coord_factor = 10000000.0
+    lat_1 = start.latitude / coord_factor
+    lat_2 = end.latitude / coord_factor
+    lon_1 = start.longitude / coord_factor
+    lon_2 = end.longitude / coord_factor
+    lat_rad_1 = math.radians(lat_1)
+    lat_rad_2 = math.radians(lat_2)
+    delta_lat_rad = math.radians(lat_2 - lat_1)
+    delta_lon_rad = math.radians(lon_2 - lon_1)
+
+    # Formula is based on http://mathforum.org/library/drmath/view/51879.html
+    a = pow(math.sin(delta_lat_rad / 2), 2) + (
+        math.cos(lat_rad_1)
+        * math.cos(lat_rad_2)
+        * pow(math.sin(delta_lon_rad / 2), 2)
+    )
+    c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
+    R = 6371000
+    # metres
+    return R * c
+
+
+class RouteGuideServicer(route_guide_pb2_grpc.RouteGuideServicer):
+    """Provides methods that implement functionality of route guide server."""
+
+    def __init__(self) -> None:
+        self.db = route_guide_resources.read_route_guide_database()
+
+    async def GetFeature(
+        self, request: route_guide_pb2.Point, unused_context
+    ) -> route_guide_pb2.Feature:
+        feature = get_feature(self.db, request)
+        if feature is None:
+            return route_guide_pb2.Feature(name="", location=request)
+        else:
+            return feature
+
+    async def ListFeatures(
+        self, request: route_guide_pb2.Rectangle, unused_context
+    ) -> AsyncIterable[route_guide_pb2.Feature]:
+        left = min(request.lo.longitude, request.hi.longitude)
+        right = max(request.lo.longitude, request.hi.longitude)
+        top = max(request.lo.latitude, request.hi.latitude)
+        bottom = min(request.lo.latitude, request.hi.latitude)
+        for feature in self.db:
+            if (
+                feature.location.longitude >= left
+                and feature.location.longitude <= right
+                and feature.location.latitude >= bottom
+                and feature.location.latitude <= top
+            ):
+                yield feature
+
+    async def RecordRoute(
+        self,
+        request_iterator: AsyncIterable[route_guide_pb2.Point],
+        unused_context,
+    ) -> route_guide_pb2.RouteSummary:
+        point_count = 0
+        feature_count = 0
+        distance = 0.0
+        prev_point = None
+
+        start_time = time.time()
+        async for point in request_iterator:
+            point_count += 1
+            if get_feature(self.db, point):
+                feature_count += 1
+            if prev_point:
+                distance += get_distance(prev_point, point)
+            prev_point = point
+
+        elapsed_time = time.time() - start_time
+        return route_guide_pb2.RouteSummary(
+            point_count=point_count,
+            feature_count=feature_count,
+            distance=int(distance),
+            elapsed_time=int(elapsed_time),
+        )
+
+    async def RouteChat(
+        self,
+        request_iterator: AsyncIterable[route_guide_pb2.RouteNote],
+        unused_context,
+    ) -> AsyncIterable[route_guide_pb2.RouteNote]:
+        prev_notes = []
+        async for new_note in request_iterator:
+            for prev_note in prev_notes:
+                if prev_note.location == new_note.location:
+                    yield prev_note
+            prev_notes.append(new_note)
+
+
+async def serve() -> None:
+    server = grpc.aio.server(interceptors=interceptors)
+    route_guide_pb2_grpc.add_RouteGuideServicer_to_server(
+        RouteGuideServicer(), server
+    )
+    server.add_insecure_port("[::]:50051")
+    await server.start()
+    await server.wait_for_termination()
+
+
+if __name__ == "__main__":
+    logging.basicConfig(level=logging.INFO)
+    asyncio.get_event_loop().run_until_complete(serve())
diff --git a/testapps/grpc-py/pinpoint.py.grpc.png b/testapps/grpc-py/pinpoint.py.grpc.png
new file mode 100644
index 00000000..103dd9af
Binary files /dev/null and b/testapps/grpc-py/pinpoint.py.grpc.png differ
diff --git a/testapps/grpc-py/readme.md b/testapps/grpc-py/readme.md
new file mode 100644
index 00000000..457db342
--- /dev/null
+++ b/testapps/grpc-py/readme.md
@@ -0,0 +1,25 @@
+test grpc-python example copy from https://github.com/grpc/grpc/tree/master/examples/python/multiplex
+
+> if you have any question, please create an issue with a `grpc-python` tag, thanks  
+
+
+### Guide 
+
+1. import `pinpointPy` into your project
+2. active `PinpointServerInterceptor` on your `grpc.server`
+```py
+   server = grpc.server(futures.ThreadPoolExecutor(
+        max_workers=10), interceptors=[PinpointServerInterceptor()])
+    route_guide_pb2_grpc.add_RouteGuideServicer_to_server(
+        RouteGuideServicer(), server
+    )
+```
+
+>PS: run `python route_guide_client.py`
+
+#### Grpc in pinpoint-py
+
+> ❗ EndPoint does not support
+
+![pinpoint.py.grpc](pinpoint.py.grpc.png)
+
diff --git a/testapps/grpc-py/requirements.txt b/testapps/grpc-py/requirements.txt
new file mode 100644
index 00000000..c9aa07fd
--- /dev/null
+++ b/testapps/grpc-py/requirements.txt
@@ -0,0 +1,5 @@
+grpc-interceptor==0.15.4
+grpcio==1.62.1
+grpcio-tools==1.62.1
+protobuf==4.25.3
+redis==5.0.1
\ No newline at end of file
diff --git a/testapps/grpc-py/route_guide_client.py b/testapps/grpc-py/route_guide_client.py
new file mode 100644
index 00000000..0fb4f34e
--- /dev/null
+++ b/testapps/grpc-py/route_guide_client.py
@@ -0,0 +1,123 @@
+# Copyright 2015 gRPC authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""The Python implementation of the gRPC route guide client."""
+
+from __future__ import print_function
+
+import logging
+import random
+
+import grpc
+import route_guide_pb2
+import route_guide_pb2_grpc
+import route_guide_resources
+
+
+def make_route_note(message, latitude, longitude):
+    return route_guide_pb2.RouteNote(
+        message=message,
+        location=route_guide_pb2.Point(latitude=latitude, longitude=longitude),
+    )
+
+
+def guide_get_one_feature(stub, point):
+    feature = stub.GetFeature(point)
+    if not feature.location:
+        print("Server returned incomplete feature")
+        return
+
+    if feature.name:
+        print("Feature called %s at %s" % (feature.name, feature.location))
+    else:
+        print("Found no feature at %s" % feature.location)
+
+
+def guide_get_feature(stub):
+    guide_get_one_feature(
+        stub, route_guide_pb2.Point(latitude=409146138, longitude=-746188906)
+    )
+    guide_get_one_feature(stub, route_guide_pb2.Point(latitude=0, longitude=0))
+
+
+def guide_list_features(stub):
+    rectangle = route_guide_pb2.Rectangle(
+        lo=route_guide_pb2.Point(latitude=400000000, longitude=-750000000),
+        hi=route_guide_pb2.Point(latitude=420000000, longitude=-730000000),
+    )
+    print("Looking for features between 40, -75 and 42, -73")
+
+    features = stub.ListFeatures(rectangle)
+
+    for feature in features:
+        print("Feature called %s at %s" % (feature.name, feature.location))
+
+
+def generate_route(feature_list):
+    for _ in range(0, 10):
+        random_feature = feature_list[random.randint(0, len(feature_list) - 1)]
+        print("Visiting point %s" % random_feature.location)
+        yield random_feature.location
+
+
+def guide_record_route(stub):
+    feature_list = route_guide_resources.read_route_guide_database()
+
+    route_iterator = generate_route(feature_list)
+    route_summary = stub.RecordRoute(route_iterator)
+    print("Finished trip with %s points " % route_summary.point_count)
+    print("Passed %s features " % route_summary.feature_count)
+    print("Travelled %s meters " % route_summary.distance)
+    print("It took %s seconds " % route_summary.elapsed_time)
+
+
+def generate_messages():
+    messages = [
+        make_route_note("First message", 0, 0),
+        make_route_note("Second message", 0, 1),
+        make_route_note("Third message", 1, 0),
+        make_route_note("Fourth message", 0, 0),
+        make_route_note("Fifth message", 1, 0),
+    ]
+    for msg in messages:
+        print("Sending %s at %s" % (msg.message, msg.location))
+        yield msg
+
+
+def guide_route_chat(stub):
+    responses = stub.RouteChat(generate_messages())
+    for response in responses:
+        print(
+            "Received message %s at %s" % (response.message, response.location)
+        )
+
+
+def run():
+    # NOTE(gRPC Python Team): .close() is possible on a channel and should be
+    # used in circumstances in which the with statement does not fit the needs
+    # of the code.
+    with grpc.insecure_channel("grpc-py:50050") as channel:
+        stub = route_guide_pb2_grpc.RouteGuideStub(channel)
+        print("-------------- GetFeature --------------")
+        guide_get_feature(stub)
+        print("-------------- ListFeatures --------------")
+        guide_list_features(stub)
+        print("-------------- RecordRoute --------------")
+        guide_record_route(stub)
+        print("-------------- RouteChat --------------")
+        guide_route_chat(stub)
+
+
+if __name__ == "__main__":
+    logging.basicConfig()
+    run()
diff --git a/testapps/grpc-py/route_guide_db.json b/testapps/grpc-py/route_guide_db.json
new file mode 100644
index 00000000..9d6a980a
--- /dev/null
+++ b/testapps/grpc-py/route_guide_db.json
@@ -0,0 +1,601 @@
+[{
+    "location": {
+        "latitude": 407838351,
+        "longitude": -746143763
+    },
+    "name": "Patriots Path, Mendham, NJ 07945, USA"
+}, {
+    "location": {
+        "latitude": 408122808,
+        "longitude": -743999179
+    },
+    "name": "101 New Jersey 10, Whippany, NJ 07981, USA"
+}, {
+    "location": {
+        "latitude": 413628156,
+        "longitude": -749015468
+    },
+    "name": "U.S. 6, Shohola, PA 18458, USA"
+}, {
+    "location": {
+        "latitude": 419999544,
+        "longitude": -740371136
+    },
+    "name": "5 Conners Road, Kingston, NY 12401, USA"
+}, {
+    "location": {
+        "latitude": 414008389,
+        "longitude": -743951297
+    },
+    "name": "Mid Hudson Psychiatric Center, New Hampton, NY 10958, USA"
+}, {
+    "location": {
+        "latitude": 419611318,
+        "longitude": -746524769
+    },
+    "name": "287 Flugertown Road, Livingston Manor, NY 12758, USA"
+}, {
+    "location": {
+        "latitude": 406109563,
+        "longitude": -742186778
+    },
+    "name": "4001 Tremley Point Road, Linden, NJ 07036, USA"
+}, {
+    "location": {
+        "latitude": 416802456,
+        "longitude": -742370183
+    },
+    "name": "352 South Mountain Road, Wallkill, NY 12589, USA"
+}, {
+    "location": {
+        "latitude": 412950425,
+        "longitude": -741077389
+    },
+    "name": "Bailey Turn Road, Harriman, NY 10926, USA"
+}, {
+    "location": {
+        "latitude": 412144655,
+        "longitude": -743949739
+    },
+    "name": "193-199 Wawayanda Road, Hewitt, NJ 07421, USA"
+}, {
+    "location": {
+        "latitude": 415736605,
+        "longitude": -742847522
+    },
+    "name": "406-496 Ward Avenue, Pine Bush, NY 12566, USA"
+}, {
+    "location": {
+        "latitude": 413843930,
+        "longitude": -740501726
+    },
+    "name": "162 Merrill Road, Highland Mills, NY 10930, USA"
+}, {
+    "location": {
+        "latitude": 410873075,
+        "longitude": -744459023
+    },
+    "name": "Clinton Road, West Milford, NJ 07480, USA"
+}, {
+    "location": {
+        "latitude": 412346009,
+        "longitude": -744026814
+    },
+    "name": "16 Old Brook Lane, Warwick, NY 10990, USA"
+}, {
+    "location": {
+        "latitude": 402948455,
+        "longitude": -747903913
+    },
+    "name": "3 Drake Lane, Pennington, NJ 08534, USA"
+}, {
+    "location": {
+        "latitude": 406337092,
+        "longitude": -740122226
+    },
+    "name": "6324 8th Avenue, Brooklyn, NY 11220, USA"
+}, {
+    "location": {
+        "latitude": 406421967,
+        "longitude": -747727624
+    },
+    "name": "1 Merck Access Road, Whitehouse Station, NJ 08889, USA"
+}, {
+    "location": {
+        "latitude": 416318082,
+        "longitude": -749677716
+    },
+    "name": "78-98 Schalck Road, Narrowsburg, NY 12764, USA"
+}, {
+    "location": {
+        "latitude": 415301720,
+        "longitude": -748416257
+    },
+    "name": "282 Lakeview Drive Road, Highland Lake, NY 12743, USA"
+}, {
+    "location": {
+        "latitude": 402647019,
+        "longitude": -747071791
+    },
+    "name": "330 Evelyn Avenue, Hamilton Township, NJ 08619, USA"
+}, {
+    "location": {
+        "latitude": 412567807,
+        "longitude": -741058078
+    },
+    "name": "New York State Reference Route 987E, Southfields, NY 10975, USA"
+}, {
+    "location": {
+        "latitude": 416855156,
+        "longitude": -744420597
+    },
+    "name": "103-271 Tempaloni Road, Ellenville, NY 12428, USA"
+}, {
+    "location": {
+        "latitude": 404663628,
+        "longitude": -744820157
+    },
+    "name": "1300 Airport Road, North Brunswick Township, NJ 08902, USA"
+}, {
+    "location": {
+        "latitude": 407113723,
+        "longitude": -749746483
+    },
+    "name": ""
+}, {
+    "location": {
+        "latitude": 402133926,
+        "longitude": -743613249
+    },
+    "name": ""
+}, {
+    "location": {
+        "latitude": 400273442,
+        "longitude": -741220915
+    },
+    "name": ""
+}, {
+    "location": {
+        "latitude": 411236786,
+        "longitude": -744070769
+    },
+    "name": ""
+}, {
+    "location": {
+        "latitude": 411633782,
+        "longitude": -746784970
+    },
+    "name": "211-225 Plains Road, Augusta, NJ 07822, USA"
+}, {
+    "location": {
+        "latitude": 415830701,
+        "longitude": -742952812
+    },
+    "name": ""
+}, {
+    "location": {
+        "latitude": 413447164,
+        "longitude": -748712898
+    },
+    "name": "165 Pedersen Ridge Road, Milford, PA 18337, USA"
+}, {
+    "location": {
+        "latitude": 405047245,
+        "longitude": -749800722
+    },
+    "name": "100-122 Locktown Road, Frenchtown, NJ 08825, USA"
+}, {
+    "location": {
+        "latitude": 418858923,
+        "longitude": -746156790
+    },
+    "name": ""
+}, {
+    "location": {
+        "latitude": 417951888,
+        "longitude": -748484944
+    },
+    "name": "650-652 Willi Hill Road, Swan Lake, NY 12783, USA"
+}, {
+    "location": {
+        "latitude": 407033786,
+        "longitude": -743977337
+    },
+    "name": "26 East 3rd Street, New Providence, NJ 07974, USA"
+}, {
+    "location": {
+        "latitude": 417548014,
+        "longitude": -740075041
+    },
+    "name": ""
+}, {
+    "location": {
+        "latitude": 410395868,
+        "longitude": -744972325
+    },
+    "name": ""
+}, {
+    "location": {
+        "latitude": 404615353,
+        "longitude": -745129803
+    },
+    "name": ""
+}, {
+    "location": {
+        "latitude": 406589790,
+        "longitude": -743560121
+    },
+    "name": "611 Lawrence Avenue, Westfield, NJ 07090, USA"
+}, {
+    "location": {
+        "latitude": 414653148,
+        "longitude": -740477477
+    },
+    "name": "18 Lannis Avenue, New Windsor, NY 12553, USA"
+}, {
+    "location": {
+        "latitude": 405957808,
+        "longitude": -743255336
+    },
+    "name": "82-104 Amherst Avenue, Colonia, NJ 07067, USA"
+}, {
+    "location": {
+        "latitude": 411733589,
+        "longitude": -741648093
+    },
+    "name": "170 Seven Lakes Drive, Sloatsburg, NY 10974, USA"
+}, {
+    "location": {
+        "latitude": 412676291,
+        "longitude": -742606606
+    },
+    "name": "1270 Lakes Road, Monroe, NY 10950, USA"
+}, {
+    "location": {
+        "latitude": 409224445,
+        "longitude": -748286738
+    },
+    "name": "509-535 Alphano Road, Great Meadows, NJ 07838, USA"
+}, {
+    "location": {
+        "latitude": 406523420,
+        "longitude": -742135517
+    },
+    "name": "652 Garden Street, Elizabeth, NJ 07202, USA"
+}, {
+    "location": {
+        "latitude": 401827388,
+        "longitude": -740294537
+    },
+    "name": "349 Sea Spray Court, Neptune City, NJ 07753, USA"
+}, {
+    "location": {
+        "latitude": 410564152,
+        "longitude": -743685054
+    },
+    "name": "13-17 Stanley Street, West Milford, NJ 07480, USA"
+}, {
+    "location": {
+        "latitude": 408472324,
+        "longitude": -740726046
+    },
+    "name": "47 Industrial Avenue, Teterboro, NJ 07608, USA"
+}, {
+    "location": {
+        "latitude": 412452168,
+        "longitude": -740214052
+    },
+    "name": "5 White Oak Lane, Stony Point, NY 10980, USA"
+}, {
+    "location": {
+        "latitude": 409146138,
+        "longitude": -746188906
+    },
+    "name": "Berkshire Valley Management Area Trail, Jefferson, NJ, USA"
+}, {
+    "location": {
+        "latitude": 404701380,
+        "longitude": -744781745
+    },
+    "name": "1007 Jersey Avenue, New Brunswick, NJ 08901, USA"
+}, {
+    "location": {
+        "latitude": 409642566,
+        "longitude": -746017679
+    },
+    "name": "6 East Emerald Isle Drive, Lake Hopatcong, NJ 07849, USA"
+}, {
+    "location": {
+        "latitude": 408031728,
+        "longitude": -748645385
+    },
+    "name": "1358-1474 New Jersey 57, Port Murray, NJ 07865, USA"
+}, {
+    "location": {
+        "latitude": 413700272,
+        "longitude": -742135189
+    },
+    "name": "367 Prospect Road, Chester, NY 10918, USA"
+}, {
+    "location": {
+        "latitude": 404310607,
+        "longitude": -740282632
+    },
+    "name": "10 Simon Lake Drive, Atlantic Highlands, NJ 07716, USA"
+}, {
+    "location": {
+        "latitude": 409319800,
+        "longitude": -746201391
+    },
+    "name": "11 Ward Street, Mount Arlington, NJ 07856, USA"
+}, {
+    "location": {
+        "latitude": 406685311,
+        "longitude": -742108603
+    },
+    "name": "300-398 Jefferson Avenue, Elizabeth, NJ 07201, USA"
+}, {
+    "location": {
+        "latitude": 419018117,
+        "longitude": -749142781
+    },
+    "name": "43 Dreher Road, Roscoe, NY 12776, USA"
+}, {
+    "location": {
+        "latitude": 412856162,
+        "longitude": -745148837
+    },
+    "name": "Swan Street, Pine Island, NY 10969, USA"
+}, {
+    "location": {
+        "latitude": 416560744,
+        "longitude": -746721964
+    },
+    "name": "66 Pleasantview Avenue, Monticello, NY 12701, USA"
+}, {
+    "location": {
+        "latitude": 405314270,
+        "longitude": -749836354
+    },
+    "name": ""
+}, {
+    "location": {
+        "latitude": 414219548,
+        "longitude": -743327440
+    },
+    "name": ""
+}, {
+    "location": {
+        "latitude": 415534177,
+        "longitude": -742900616
+    },
+    "name": "565 Winding Hills Road, Montgomery, NY 12549, USA"
+}, {
+    "location": {
+        "latitude": 406898530,
+        "longitude": -749127080
+    },
+    "name": "231 Rocky Run Road, Glen Gardner, NJ 08826, USA"
+}, {
+    "location": {
+        "latitude": 407586880,
+        "longitude": -741670168
+    },
+    "name": "100 Mount Pleasant Avenue, Newark, NJ 07104, USA"
+}, {
+    "location": {
+        "latitude": 400106455,
+        "longitude": -742870190
+    },
+    "name": "517-521 Huntington Drive, Manchester Township, NJ 08759, USA"
+}, {
+    "location": {
+        "latitude": 400066188,
+        "longitude": -746793294
+    },
+    "name": ""
+}, {
+    "location": {
+        "latitude": 418803880,
+        "longitude": -744102673
+    },
+    "name": "40 Mountain Road, Napanoch, NY 12458, USA"
+}, {
+    "location": {
+        "latitude": 414204288,
+        "longitude": -747895140
+    },
+    "name": ""
+}, {
+    "location": {
+        "latitude": 414777405,
+        "longitude": -740615601
+    },
+    "name": ""
+}, {
+    "location": {
+        "latitude": 415464475,
+        "longitude": -747175374
+    },
+    "name": "48 North Road, Forestburgh, NY 12777, USA"
+}, {
+    "location": {
+        "latitude": 404062378,
+        "longitude": -746376177
+    },
+    "name": ""
+}, {
+    "location": {
+        "latitude": 405688272,
+        "longitude": -749285130
+    },
+    "name": ""
+}, {
+    "location": {
+        "latitude": 400342070,
+        "longitude": -748788996
+    },
+    "name": ""
+}, {
+    "location": {
+        "latitude": 401809022,
+        "longitude": -744157964
+    },
+    "name": ""
+}, {
+    "location": {
+        "latitude": 404226644,
+        "longitude": -740517141
+    },
+    "name": "9 Thompson Avenue, Leonardo, NJ 07737, USA"
+}, {
+    "location": {
+        "latitude": 410322033,
+        "longitude": -747871659
+    },
+    "name": ""
+}, {
+    "location": {
+        "latitude": 407100674,
+        "longitude": -747742727
+    },
+    "name": ""
+}, {
+    "location": {
+        "latitude": 418811433,
+        "longitude": -741718005
+    },
+    "name": "213 Bush Road, Stone Ridge, NY 12484, USA"
+}, {
+    "location": {
+        "latitude": 415034302,
+        "longitude": -743850945
+    },
+    "name": ""
+}, {
+    "location": {
+        "latitude": 411349992,
+        "longitude": -743694161
+    },
+    "name": ""
+}, {
+    "location": {
+        "latitude": 404839914,
+        "longitude": -744759616
+    },
+    "name": "1-17 Bergen Court, New Brunswick, NJ 08901, USA"
+}, {
+    "location": {
+        "latitude": 414638017,
+        "longitude": -745957854
+    },
+    "name": "35 Oakland Valley Road, Cuddebackville, NY 12729, USA"
+}, {
+    "location": {
+        "latitude": 412127800,
+        "longitude": -740173578
+    },
+    "name": ""
+}, {
+    "location": {
+        "latitude": 401263460,
+        "longitude": -747964303
+    },
+    "name": ""
+}, {
+    "location": {
+        "latitude": 412843391,
+        "longitude": -749086026
+    },
+    "name": ""
+}, {
+    "location": {
+        "latitude": 418512773,
+        "longitude": -743067823
+    },
+    "name": ""
+}, {
+    "location": {
+        "latitude": 404318328,
+        "longitude": -740835638
+    },
+    "name": "42-102 Main Street, Belford, NJ 07718, USA"
+}, {
+    "location": {
+        "latitude": 419020746,
+        "longitude": -741172328
+    },
+    "name": ""
+}, {
+    "location": {
+        "latitude": 404080723,
+        "longitude": -746119569
+    },
+    "name": ""
+}, {
+    "location": {
+        "latitude": 401012643,
+        "longitude": -744035134
+    },
+    "name": ""
+}, {
+    "location": {
+        "latitude": 404306372,
+        "longitude": -741079661
+    },
+    "name": ""
+}, {
+    "location": {
+        "latitude": 403966326,
+        "longitude": -748519297
+    },
+    "name": ""
+}, {
+    "location": {
+        "latitude": 405002031,
+        "longitude": -748407866
+    },
+    "name": ""
+}, {
+    "location": {
+        "latitude": 409532885,
+        "longitude": -742200683
+    },
+    "name": ""
+}, {
+    "location": {
+        "latitude": 416851321,
+        "longitude": -742674555
+    },
+    "name": ""
+}, {
+    "location": {
+        "latitude": 406411633,
+        "longitude": -741722051
+    },
+    "name": "3387 Richmond Terrace, Staten Island, NY 10303, USA"
+}, {
+    "location": {
+        "latitude": 413069058,
+        "longitude": -744597778
+    },
+    "name": "261 Van Sickle Road, Goshen, NY 10924, USA"
+}, {
+    "location": {
+        "latitude": 418465462,
+        "longitude": -746859398
+    },
+    "name": ""
+}, {
+    "location": {
+        "latitude": 411733222,
+        "longitude": -744228360
+    },
+    "name": ""
+}, {
+    "location": {
+        "latitude": 410248224,
+        "longitude": -747127767
+    },
+    "name": "3 Hasta Way, Newton, NJ 07860, USA"
+}]
diff --git a/testapps/grpc-py/route_guide_pb2.py b/testapps/grpc-py/route_guide_pb2.py
new file mode 100644
index 00000000..fc67fe0c
--- /dev/null
+++ b/testapps/grpc-py/route_guide_pb2.py
@@ -0,0 +1,37 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler.  DO NOT EDIT!
+# source: route_guide.proto
+# Protobuf Python Version: 4.25.0
+"""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\x11route_guide.proto\x12\nrouteguide\",\n\x05Point\x12\x10\n\x08latitude\x18\x01 \x01(\x05\x12\x11\n\tlongitude\x18\x02 \x01(\x05\"I\n\tRectangle\x12\x1d\n\x02lo\x18\x01 \x01(\x0b\x32\x11.routeguide.Point\x12\x1d\n\x02hi\x18\x02 \x01(\x0b\x32\x11.routeguide.Point\"<\n\x07\x46\x65\x61ture\x12\x0c\n\x04name\x18\x01 \x01(\t\x12#\n\x08location\x18\x02 \x01(\x0b\x32\x11.routeguide.Point\"A\n\tRouteNote\x12#\n\x08location\x18\x01 \x01(\x0b\x32\x11.routeguide.Point\x12\x0f\n\x07message\x18\x02 \x01(\t\"b\n\x0cRouteSummary\x12\x13\n\x0bpoint_count\x18\x01 \x01(\x05\x12\x15\n\rfeature_count\x18\x02 \x01(\x05\x12\x10\n\x08\x64istance\x18\x03 \x01(\x05\x12\x14\n\x0c\x65lapsed_time\x18\x04 \x01(\x05\x32\x85\x02\n\nRouteGuide\x12\x36\n\nGetFeature\x12\x11.routeguide.Point\x1a\x13.routeguide.Feature\"\x00\x12>\n\x0cListFeatures\x12\x15.routeguide.Rectangle\x1a\x13.routeguide.Feature\"\x00\x30\x01\x12>\n\x0bRecordRoute\x12\x11.routeguide.Point\x1a\x18.routeguide.RouteSummary\"\x00(\x01\x12?\n\tRouteChat\x12\x15.routeguide.RouteNote\x1a\x15.routeguide.RouteNote\"\x00(\x01\x30\x01\x42\x36\n\x1bio.grpc.examples.routeguideB\x0fRouteGuideProtoP\x01\xa2\x02\x03RTGb\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'route_guide_pb2', _globals)
+if _descriptor._USE_C_DESCRIPTORS == False:
+  _globals['DESCRIPTOR']._options = None
+  _globals['DESCRIPTOR']._serialized_options = b'\n\033io.grpc.examples.routeguideB\017RouteGuideProtoP\001\242\002\003RTG'
+  _globals['_POINT']._serialized_start=33
+  _globals['_POINT']._serialized_end=77
+  _globals['_RECTANGLE']._serialized_start=79
+  _globals['_RECTANGLE']._serialized_end=152
+  _globals['_FEATURE']._serialized_start=154
+  _globals['_FEATURE']._serialized_end=214
+  _globals['_ROUTENOTE']._serialized_start=216
+  _globals['_ROUTENOTE']._serialized_end=281
+  _globals['_ROUTESUMMARY']._serialized_start=283
+  _globals['_ROUTESUMMARY']._serialized_end=381
+  _globals['_ROUTEGUIDE']._serialized_start=384
+  _globals['_ROUTEGUIDE']._serialized_end=645
+# @@protoc_insertion_point(module_scope)
diff --git a/testapps/grpc-py/route_guide_pb2.pyi b/testapps/grpc-py/route_guide_pb2.pyi
new file mode 100644
index 00000000..3376135d
--- /dev/null
+++ b/testapps/grpc-py/route_guide_pb2.pyi
@@ -0,0 +1,49 @@
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import message as _message
+from typing import ClassVar as _ClassVar, Mapping as _Mapping, Optional as _Optional, Union as _Union
+
+DESCRIPTOR: _descriptor.FileDescriptor
+
+class Point(_message.Message):
+    __slots__ = ("latitude", "longitude")
+    LATITUDE_FIELD_NUMBER: _ClassVar[int]
+    LONGITUDE_FIELD_NUMBER: _ClassVar[int]
+    latitude: int
+    longitude: int
+    def __init__(self, latitude: _Optional[int] = ..., longitude: _Optional[int] = ...) -> None: ...
+
+class Rectangle(_message.Message):
+    __slots__ = ("lo", "hi")
+    LO_FIELD_NUMBER: _ClassVar[int]
+    HI_FIELD_NUMBER: _ClassVar[int]
+    lo: Point
+    hi: Point
+    def __init__(self, lo: _Optional[_Union[Point, _Mapping]] = ..., hi: _Optional[_Union[Point, _Mapping]] = ...) -> None: ...
+
+class Feature(_message.Message):
+    __slots__ = ("name", "location")
+    NAME_FIELD_NUMBER: _ClassVar[int]
+    LOCATION_FIELD_NUMBER: _ClassVar[int]
+    name: str
+    location: Point
+    def __init__(self, name: _Optional[str] = ..., location: _Optional[_Union[Point, _Mapping]] = ...) -> None: ...
+
+class RouteNote(_message.Message):
+    __slots__ = ("location", "message")
+    LOCATION_FIELD_NUMBER: _ClassVar[int]
+    MESSAGE_FIELD_NUMBER: _ClassVar[int]
+    location: Point
+    message: str
+    def __init__(self, location: _Optional[_Union[Point, _Mapping]] = ..., message: _Optional[str] = ...) -> None: ...
+
+class RouteSummary(_message.Message):
+    __slots__ = ("point_count", "feature_count", "distance", "elapsed_time")
+    POINT_COUNT_FIELD_NUMBER: _ClassVar[int]
+    FEATURE_COUNT_FIELD_NUMBER: _ClassVar[int]
+    DISTANCE_FIELD_NUMBER: _ClassVar[int]
+    ELAPSED_TIME_FIELD_NUMBER: _ClassVar[int]
+    point_count: int
+    feature_count: int
+    distance: int
+    elapsed_time: int
+    def __init__(self, point_count: _Optional[int] = ..., feature_count: _Optional[int] = ..., distance: _Optional[int] = ..., elapsed_time: _Optional[int] = ...) -> None: ...
diff --git a/testapps/grpc-py/route_guide_pb2_grpc.py b/testapps/grpc-py/route_guide_pb2_grpc.py
new file mode 100644
index 00000000..3641d2e0
--- /dev/null
+++ b/testapps/grpc-py/route_guide_pb2_grpc.py
@@ -0,0 +1,188 @@
+# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
+"""Client and server classes corresponding to protobuf-defined services."""
+import grpc
+
+import route_guide_pb2 as route__guide__pb2
+
+
+class RouteGuideStub(object):
+    """Interface exported by the server.
+    """
+
+    def __init__(self, channel):
+        """Constructor.
+
+        Args:
+            channel: A grpc.Channel.
+        """
+        self.GetFeature = channel.unary_unary(
+                '/routeguide.RouteGuide/GetFeature',
+                request_serializer=route__guide__pb2.Point.SerializeToString,
+                response_deserializer=route__guide__pb2.Feature.FromString,
+                )
+        self.ListFeatures = channel.unary_stream(
+                '/routeguide.RouteGuide/ListFeatures',
+                request_serializer=route__guide__pb2.Rectangle.SerializeToString,
+                response_deserializer=route__guide__pb2.Feature.FromString,
+                )
+        self.RecordRoute = channel.stream_unary(
+                '/routeguide.RouteGuide/RecordRoute',
+                request_serializer=route__guide__pb2.Point.SerializeToString,
+                response_deserializer=route__guide__pb2.RouteSummary.FromString,
+                )
+        self.RouteChat = channel.stream_stream(
+                '/routeguide.RouteGuide/RouteChat',
+                request_serializer=route__guide__pb2.RouteNote.SerializeToString,
+                response_deserializer=route__guide__pb2.RouteNote.FromString,
+                )
+
+
+class RouteGuideServicer(object):
+    """Interface exported by the server.
+    """
+
+    def GetFeature(self, request, context):
+        """A simple RPC.
+
+        Obtains the feature at a given position.
+
+        A feature with an empty name is returned if there's no feature at the given
+        position.
+        """
+        context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+        context.set_details('Method not implemented!')
+        raise NotImplementedError('Method not implemented!')
+
+    def ListFeatures(self, request, context):
+        """A server-to-client streaming RPC.
+
+        Obtains the Features available within the given Rectangle.  Results are
+        streamed rather than returned at once (e.g. in a response message with a
+        repeated field), as the rectangle may cover a large area and contain a
+        huge number of features.
+        """
+        context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+        context.set_details('Method not implemented!')
+        raise NotImplementedError('Method not implemented!')
+
+    def RecordRoute(self, request_iterator, context):
+        """A client-to-server streaming RPC.
+
+        Accepts a stream of Points on a route being traversed, returning a
+        RouteSummary when traversal is completed.
+        """
+        context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+        context.set_details('Method not implemented!')
+        raise NotImplementedError('Method not implemented!')
+
+    def RouteChat(self, request_iterator, context):
+        """A Bidirectional streaming RPC.
+
+        Accepts a stream of RouteNotes sent while a route is being traversed,
+        while receiving other RouteNotes (e.g. from other users).
+        """
+        context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+        context.set_details('Method not implemented!')
+        raise NotImplementedError('Method not implemented!')
+
+
+def add_RouteGuideServicer_to_server(servicer, server):
+    rpc_method_handlers = {
+            'GetFeature': grpc.unary_unary_rpc_method_handler(
+                    servicer.GetFeature,
+                    request_deserializer=route__guide__pb2.Point.FromString,
+                    response_serializer=route__guide__pb2.Feature.SerializeToString,
+            ),
+            'ListFeatures': grpc.unary_stream_rpc_method_handler(
+                    servicer.ListFeatures,
+                    request_deserializer=route__guide__pb2.Rectangle.FromString,
+                    response_serializer=route__guide__pb2.Feature.SerializeToString,
+            ),
+            'RecordRoute': grpc.stream_unary_rpc_method_handler(
+                    servicer.RecordRoute,
+                    request_deserializer=route__guide__pb2.Point.FromString,
+                    response_serializer=route__guide__pb2.RouteSummary.SerializeToString,
+            ),
+            'RouteChat': grpc.stream_stream_rpc_method_handler(
+                    servicer.RouteChat,
+                    request_deserializer=route__guide__pb2.RouteNote.FromString,
+                    response_serializer=route__guide__pb2.RouteNote.SerializeToString,
+            ),
+    }
+    generic_handler = grpc.method_handlers_generic_handler(
+            'routeguide.RouteGuide', rpc_method_handlers)
+    server.add_generic_rpc_handlers((generic_handler,))
+
+
+ # This class is part of an EXPERIMENTAL API.
+class RouteGuide(object):
+    """Interface exported by the server.
+    """
+
+    @staticmethod
+    def GetFeature(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, '/routeguide.RouteGuide/GetFeature',
+            route__guide__pb2.Point.SerializeToString,
+            route__guide__pb2.Feature.FromString,
+            options, channel_credentials,
+            insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
+
+    @staticmethod
+    def ListFeatures(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, '/routeguide.RouteGuide/ListFeatures',
+            route__guide__pb2.Rectangle.SerializeToString,
+            route__guide__pb2.Feature.FromString,
+            options, channel_credentials,
+            insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
+
+    @staticmethod
+    def RecordRoute(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, '/routeguide.RouteGuide/RecordRoute',
+            route__guide__pb2.Point.SerializeToString,
+            route__guide__pb2.RouteSummary.FromString,
+            options, channel_credentials,
+            insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
+
+    @staticmethod
+    def RouteChat(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, '/routeguide.RouteGuide/RouteChat',
+            route__guide__pb2.RouteNote.SerializeToString,
+            route__guide__pb2.RouteNote.FromString,
+            options, channel_credentials,
+            insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
diff --git a/testapps/grpc-py/route_guide_resources.py b/testapps/grpc-py/route_guide_resources.py
new file mode 100644
index 00000000..7cee03ad
--- /dev/null
+++ b/testapps/grpc-py/route_guide_resources.py
@@ -0,0 +1,39 @@
+# Copyright 2015 gRPC authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Common resources used in the gRPC route guide example."""
+
+import json
+
+import route_guide_pb2
+
+
+def read_route_guide_database():
+    """Reads the route guide database.
+
+    Returns:
+      The full contents of the route guide database as a sequence of
+        route_guide_pb2.Features.
+    """
+    feature_list = []
+    with open("route_guide_db.json") as route_guide_db_file:
+        for item in json.load(route_guide_db_file):
+            feature = route_guide_pb2.Feature(
+                name=item["name"],
+                location=route_guide_pb2.Point(
+                    latitude=item["location"]["latitude"],
+                    longitude=item["location"]["longitude"],
+                ),
+            )
+            feature_list.append(feature)
+    return feature_list
diff --git a/testapps/grpc-py/route_guide_server.py b/testapps/grpc-py/route_guide_server.py
new file mode 100644
index 00000000..a2ced095
--- /dev/null
+++ b/testapps/grpc-py/route_guide_server.py
@@ -0,0 +1,144 @@
+# Copyright 2015 gRPC authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""The Python implementation of the gRPC route guide server."""
+
+from pinpointPy import set_agent, monkey_patch_for_pinpoint, use_thread_local_context
+from concurrent import futures
+import logging
+import math
+import time
+import redis
+
+import grpc
+import route_guide_pb2
+import route_guide_pb2_grpc
+import route_guide_resources
+from pinpointPy.grpc_ import PinpointServerInterceptor
+
+
+use_thread_local_context()
+monkey_patch_for_pinpoint()
+set_agent("cd.dev.test.grpc", "cd.dev.test.py",
+          'tcp:dev-collector:10000', -1, 0, logging.DEBUG)
+
+
+def get_feature(feature_db, point):
+    for feature in feature_db:
+        if feature.location == point:
+            return feature
+    return None
+
+
+def get_distance(start, end):
+    coord_factor = 10000000.0
+    lat_1 = start.latitude / coord_factor
+    lat_2 = end.latitude / coord_factor
+    lon_1 = start.longitude / coord_factor
+    lon_2 = end.longitude / coord_factor
+    lat_rad_1 = math.radians(lat_1)
+    lat_rad_2 = math.radians(lat_2)
+    delta_lat_rad = math.radians(lat_2 - lat_1)
+    delta_lon_rad = math.radians(lon_2 - lon_1)
+
+    # Formula is based on http://mathforum.org/library/drmath/view/51879.html
+    a = pow(math.sin(delta_lat_rad / 2), 2) + (
+        math.cos(lat_rad_1)
+        * math.cos(lat_rad_2)
+        * pow(math.sin(delta_lon_rad / 2), 2)
+    )
+    c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
+    R = 6371000
+    # metres
+    return R * c
+
+
+class RouteGuideServicer(route_guide_pb2_grpc.RouteGuideServicer):
+    """Provides methods that implement functionality of route guide server."""
+
+    def __init__(self):
+        self.db = route_guide_resources.read_route_guide_database()
+
+    def testRedis(self):
+        conn = redis.Redis(host='redis', port=6379)
+        conn.set("abc", "a long value")
+        conn.get("abc")
+
+    def GetFeature(self, request, context):
+        feature = get_feature(self.db, request)
+        self.testRedis()
+        if feature is None:
+            return route_guide_pb2.Feature(name="", location=request)
+        else:
+            return feature
+
+    def ListFeatures(self, request, context):
+        left = min(request.lo.longitude, request.hi.longitude)
+        right = max(request.lo.longitude, request.hi.longitude)
+        top = max(request.lo.latitude, request.hi.latitude)
+        bottom = min(request.lo.latitude, request.hi.latitude)
+        for feature in self.db:
+            if (
+                feature.location.longitude >= left
+                and feature.location.longitude <= right
+                and feature.location.latitude >= bottom
+                and feature.location.latitude <= top
+            ):
+                yield feature
+
+    def RecordRoute(self, request_iterator, context):
+        point_count = 0
+        feature_count = 0
+        distance = 0.0
+        prev_point = None
+
+        start_time = time.time()
+        for point in request_iterator:
+            point_count += 1
+            if get_feature(self.db, point):
+                feature_count += 1
+            if prev_point:
+                distance += get_distance(prev_point, point)
+            prev_point = point
+
+        elapsed_time = time.time() - start_time
+        return route_guide_pb2.RouteSummary(
+            point_count=point_count,
+            feature_count=feature_count,
+            distance=int(distance),
+            elapsed_time=int(elapsed_time),
+        )
+
+    def RouteChat(self, request_iterator, context):
+        prev_notes = []
+        for new_note in request_iterator:
+            for prev_note in prev_notes:
+                if prev_note.location == new_note.location:
+                    yield prev_note
+            prev_notes.append(new_note)
+
+
+def serve():
+    server = grpc.server(futures.ThreadPoolExecutor(
+        max_workers=10), interceptors=[PinpointServerInterceptor()])
+    route_guide_pb2_grpc.add_RouteGuideServicer_to_server(
+        RouteGuideServicer(), server
+    )
+    server.add_insecure_port("0.0.0.0:50050")
+    server.start()
+    server.wait_for_termination()
+
+
+if __name__ == "__main__":
+    logging.basicConfig()
+    serve()
diff --git a/testapps/grpc-py/run_codegen.py b/testapps/grpc-py/run_codegen.py
new file mode 100644
index 00000000..aa237cd8
--- /dev/null
+++ b/testapps/grpc-py/run_codegen.py
@@ -0,0 +1,26 @@
+# Copyright 2015 gRPC authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Runs protoc with the gRPC plugin to generate messages and gRPC stubs."""
+
+from grpc_tools import protoc
+
+protoc.main(
+    (
+        "",
+        "-I../../protos",
+        "--python_out=.",
+        "--grpc_python_out=.",
+        "../../protos/route_guide.proto",
+    )
+)
diff --git a/testapps/grpc_py.dockerfile b/testapps/grpc_py.dockerfile
new file mode 100644
index 00000000..8b375b42
--- /dev/null
+++ b/testapps/grpc_py.dockerfile
@@ -0,0 +1,15 @@
+FROM python:3.8
+WORKDIR /app/
+
+COPY testapps/grpc-py/ /app/
+RUN pip install -r requirements.txt
+
+COPY setup.py /pinpoint-c-agent/setup.py
+COPY common/ /pinpoint-c-agent/common
+COPY README /pinpoint-c-agent/README
+COPY plugins/PY /pinpoint-c-agent/plugins/PY
+COPY src/PY /pinpoint-c-agent/src/PY
+RUN cd /pinpoint-c-agent && pip install -e .
+
+EXPOSE 50050
+CMD [ "python", "route_guide_server.py"]
\ No newline at end of file
diff --git a/testapps/grpc_py_client.dockerfile b/testapps/grpc_py_client.dockerfile
new file mode 100644
index 00000000..e07822e0
--- /dev/null
+++ b/testapps/grpc_py_client.dockerfile
@@ -0,0 +1,15 @@
+FROM python:3.8
+WORKDIR /app/
+
+COPY testapps/grpc-py/ /app/
+RUN pip install -r requirements.txt
+
+COPY setup.py /pinpoint-c-agent/setup.py
+COPY common/ /pinpoint-c-agent/common
+COPY README /pinpoint-c-agent/README
+COPY plugins/PY /pinpoint-c-agent/plugins/PY
+COPY src/PY /pinpoint-c-agent/src/PY
+RUN cd /pinpoint-c-agent && pip install -e .
+
+EXPOSE 50050
+CMD [ "python", "route_guide_client.py"]
\ No newline at end of file