We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
DroppedSpan has no attribute 'transaction' error in grpc/client_interceptor.py
We are experiencing the following error using automatic instrumentation of gRPC requests for transactions with more than 500 spans: `AttributeError: 'DroppedSpan' object has no attribute 'transaction'`. The error occurs at the following line
https://github.com/elastic/apm-agent-python/blob/479a69d61f728ec24de5fb7504c02dcbfe4f4c8a/elasticapm/contrib/grpc/client_interceptor.py#L197
**To Reproduce**

1. Start a transaction
2. Produce more spans than the transaction_max_spans
3. Call a gRPC endpoint

**Environment (please complete the following information)**
- agent version: 6.15.1


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `elasticapm/contrib/grpc/client_interceptor.py`
Content:
```
1 #  BSD 3-Clause License
2 #
3 #  Copyright (c) 2022, Elasticsearch BV
4 #  All rights reserved.
5 #
6 #  Redistribution and use in source and binary forms, with or without
7 #  modification, are permitted provided that the following conditions are met:
8 #
9 #  * Redistributions of source code must retain the above copyright notice, this
10 #    list of conditions and the following disclaimer.
11 #
12 #  * Redistributions in binary form must reproduce the above copyright notice,
13 #    this list of conditions and the following disclaimer in the documentation
14 #    and/or other materials provided with the distribution.
15 #
16 #  * Neither the name of the copyright holder nor the names of its
17 #    contributors may be used to endorse or promote products derived from
18 #    this software without specific prior written permission.
19 #
20 #  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21 #  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22 #  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23 #  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24 #  FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25 #  DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26 #  SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27 #  CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28 #  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 #  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30 
31 from typing import Optional
32 
33 import grpc
34 from grpc._interceptor import _ClientCallDetails
35 
36 import elasticapm
37 from elasticapm.conf import constants
38 from elasticapm.traces import Span
39 from elasticapm.utils import default_ports
40 
41 
42 class _ClientInterceptor(
43     grpc.UnaryUnaryClientInterceptor,
44     # grpc.UnaryStreamClientInterceptor,
45     # grpc.StreamUnaryClientInterceptor,
46     # grpc.StreamStreamClientInterceptor,
47 ):
48     def __init__(self, host: Optional[str], port: Optional[str], secure: bool):
49         self.host: str = host
50         self.port: str = port
51         self.secure: bool = secure
52         schema = "https" if secure else "http"
53         resource = f"{schema}://{host}"
54         if port and int(port) != default_ports[schema]:
55             resource += f":{port}"
56 
57         self._context = {
58             "http": {
59                 "url": resource,
60             },
61             "destination": {
62                 "address": host,
63                 "port": port,
64             },
65         }
66 
67     def intercept_unary_unary(self, continuation, client_call_details, request):
68         """Intercepts a unary-unary invocation asynchronously.
69 
70         Args:
71           continuation: A function that proceeds with the invocation by
72             executing the next interceptor in chain or invoking the
73             actual RPC on the underlying Channel. It is the interceptor's
74             responsibility to call it if it decides to move the RPC forward.
75             The interceptor can use
76             `response_future = continuation(client_call_details, request)`
77             to continue with the RPC. `continuation` returns an object that is
78             both a Call for the RPC and a Future. In the event of RPC
79             completion, the return Call-Future's result value will be
80             the response message of the RPC. Should the event terminate
81             with non-OK status, the returned Call-Future's exception value
82             will be an RpcError.
83           client_call_details: A ClientCallDetails object describing the
84             outgoing RPC.
85           request: The request value for the RPC.
86 
87         Returns:
88             An object that is both a Call for the RPC and a Future.
89             In the event of RPC completion, the return Call-Future's
90             result value will be the response message of the RPC.
91             Should the event terminate with non-OK status, the returned
92             Call-Future's exception value will be an RpcError.
93         """
94         with elasticapm.capture_span(
95             client_call_details.method, span_type="external", span_subtype="grpc", extra=self._context.copy(), leaf=True
96         ) as span:
97             if not span:
98                 return continuation(client_call_details, request)
99             client_call_details = self.attach_traceparent(client_call_details, span)
100             try:
101                 response = continuation(client_call_details, request)
102             except grpc.RpcError:
103                 span.set_failure()
104                 raise
105 
106             return response
107 
108     # TODO: instrument other types of requests once the spec is ready
109 
110     # def intercept_unary_stream(self, continuation, client_call_details,
111     #                            request):
112     #     """Intercepts a unary-stream invocation.
113     #
114     #     Args:
115     #       continuation: A function that proceeds with the invocation by
116     #         executing the next interceptor in chain or invoking the
117     #         actual RPC on the underlying Channel. It is the interceptor's
118     #         responsibility to call it if it decides to move the RPC forward.
119     #         The interceptor can use
120     #         `response_iterator = continuation(client_call_details, request)`
121     #         to continue with the RPC. `continuation` returns an object that is
122     #         both a Call for the RPC and an iterator for response values.
123     #         Drawing response values from the returned Call-iterator may
124     #         raise RpcError indicating termination of the RPC with non-OK
125     #         status.
126     #       client_call_details: A ClientCallDetails object describing the
127     #         outgoing RPC.
128     #       request: The request value for the RPC.
129     #
130     #     Returns:
131     #         An object that is both a Call for the RPC and an iterator of
132     #         response values. Drawing response values from the returned
133     #         Call-iterator may raise RpcError indicating termination of
134     #         the RPC with non-OK status. This object *should* also fulfill the
135     #         Future interface, though it may not.
136     #     """
137     #     response_iterator = continuation(client_call_details, request)
138     #     return response_iterator
139     #
140     # def intercept_stream_unary(self, continuation, client_call_details,
141     #                            request_iterator):
142     #     """Intercepts a stream-unary invocation asynchronously.
143     #
144     #     Args:
145     #       continuation: A function that proceeds with the invocation by
146     #         executing the next interceptor in chain or invoking the
147     #         actual RPC on the underlying Channel. It is the interceptor's
148     #         responsibility to call it if it decides to move the RPC forward.
149     #         The interceptor can use
150     #         `response_future = continuation(client_call_details, request_iterator)`
151     #         to continue with the RPC. `continuation` returns an object that is
152     #         both a Call for the RPC and a Future. In the event of RPC completion,
153     #         the return Call-Future's result value will be the response message
154     #         of the RPC. Should the event terminate with non-OK status, the
155     #         returned Call-Future's exception value will be an RpcError.
156     #       client_call_details: A ClientCallDetails object describing the
157     #         outgoing RPC.
158     #       request_iterator: An iterator that yields request values for the RPC.
159     #
160     #     Returns:
161     #       An object that is both a Call for the RPC and a Future.
162     #       In the event of RPC completion, the return Call-Future's
163     #       result value will be the response message of the RPC.
164     #       Should the event terminate with non-OK status, the returned
165     #       Call-Future's exception value will be an RpcError.
166     #     """
167     #
168     # def intercept_stream_stream(self, continuation, client_call_details,
169     #                             request_iterator):
170     #     """Intercepts a stream-stream invocation.
171     #
172     #     Args:
173     #       continuation: A function that proceeds with the invocation by
174     #         executing the next interceptor in chain or invoking the
175     #         actual RPC on the underlying Channel. It is the interceptor's
176     #         responsibility to call it if it decides to move the RPC forward.
177     #         The interceptor can use
178     #         `response_iterator = continuation(client_call_details, request_iterator)`
179     #         to continue with the RPC. `continuation` returns an object that is
180     #         both a Call for the RPC and an iterator for response values.
181     #         Drawing response values from the returned Call-iterator may
182     #         raise RpcError indicating termination of the RPC with non-OK
183     #         status.
184     #       client_call_details: A ClientCallDetails object describing the
185     #         outgoing RPC.
186     #       request_iterator: An iterator that yields request values for the RPC.
187     #
188     #     Returns:
189     #       An object that is both a Call for the RPC and an iterator of
190     #       response values. Drawing response values from the returned
191     #       Call-iterator may raise RpcError indicating termination of
192     #       the RPC with non-OK status. This object *should* also fulfill the
193     #       Future interface, though it may not.
194     #     """
195 
196     def attach_traceparent(self, client_call_details: _ClientCallDetails, span: Span):
197         if not span.transaction:
198             return client_call_details
199         meta = list(client_call_details.metadata) if client_call_details.metadata else []
200         if constants.TRACEPARENT_HEADER_NAME not in meta:
201             traceparent = span.transaction.trace_parent.copy_from(span_id=span.id)
202             meta.extend(
203                 (
204                     (constants.TRACEPARENT_HEADER_NAME, traceparent.to_string()),
205                     (constants.TRACESTATE_HEADER_NAME, traceparent.tracestate),
206                 )
207             )
208         return client_call_details._replace(metadata=meta)
209
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/elasticapm/contrib/grpc/client_interceptor.py b/elasticapm/contrib/grpc/client_interceptor.py
--- a/elasticapm/contrib/grpc/client_interceptor.py
+++ b/elasticapm/contrib/grpc/client_interceptor.py
@@ -35,7 +35,7 @@
 
 import elasticapm
 from elasticapm.conf import constants
-from elasticapm.traces import Span
+from elasticapm.traces import DroppedSpan, SpanType
 from elasticapm.utils import default_ports
 
 
@@ -193,8 +193,8 @@
     #       Future interface, though it may not.
     #     """
 
-    def attach_traceparent(self, client_call_details: _ClientCallDetails, span: Span):
-        if not span.transaction:
+    def attach_traceparent(self, client_call_details: _ClientCallDetails, span: SpanType) -> _ClientCallDetails:
+        if isinstance(span, DroppedSpan) or not span.transaction:
             return client_call_details
         meta = list(client_call_details.metadata) if client_call_details.metadata else []
         if constants.TRACEPARENT_HEADER_NAME not in meta:

{"golden_diff": "diff --git a/elasticapm/contrib/grpc/client_interceptor.py b/elasticapm/contrib/grpc/client_interceptor.py\n--- a/elasticapm/contrib/grpc/client_interceptor.py\n+++ b/elasticapm/contrib/grpc/client_interceptor.py\n@@ -35,7 +35,7 @@\n \n import elasticapm\n from elasticapm.conf import constants\n-from elasticapm.traces import Span\n+from elasticapm.traces import DroppedSpan, SpanType\n from elasticapm.utils import default_ports\n \n \n@@ -193,8 +193,8 @@\n     #       Future interface, though it may not.\n     #     \"\"\"\n \n-    def attach_traceparent(self, client_call_details: _ClientCallDetails, span: Span):\n-        if not span.transaction:\n+    def attach_traceparent(self, client_call_details: _ClientCallDetails, span: SpanType) -> _ClientCallDetails:\n+        if isinstance(span, DroppedSpan) or not span.transaction:\n             return client_call_details\n         meta = list(client_call_details.metadata) if client_call_details.metadata else []\n         if constants.TRACEPARENT_HEADER_NAME not in meta:\n", "issue": "DroppedSpan has no attribute 'transaction' error in grpc/client_interceptor.py\nWe are experiencing the following error using automatic instrumentation of gRPC requests for transactions with more than 500 spans: `AttributeError: 'DroppedSpan' object has no attribute 'transaction'`. The error occurs at the following line\r\nhttps://github.com/elastic/apm-agent-python/blob/479a69d61f728ec24de5fb7504c02dcbfe4f4c8a/elasticapm/contrib/grpc/client_interceptor.py#L197\r\n**To Reproduce**\r\n\r\n1. Start a transaction\r\n2. Produce more spans than the transaction_max_spans\r\n3. Call a gRPC endpoint\r\n\r\n**Environment (please complete the following information)**\r\n- agent version: 6.15.1\r\n\n", "before_files": [{"content": "#  BSD 3-Clause License\n#\n#  Copyright (c) 2022, Elasticsearch BV\n#  All rights reserved.\n#\n#  Redistribution and use in source and binary forms, with or without\n#  modification, are permitted provided that the following conditions are met:\n#\n#  * Redistributions of source code must retain the above copyright notice, this\n#    list of conditions and the following disclaimer.\n#\n#  * Redistributions in binary form must reproduce the above copyright notice,\n#    this list of conditions and the following disclaimer in the documentation\n#    and/or other materials provided with the distribution.\n#\n#  * Neither the name of the copyright holder nor the names of its\n#    contributors may be used to endorse or promote products derived from\n#    this software without specific prior written permission.\n#\n#  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\n#  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\n#  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\n#  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\n#  FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\n#  DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\n#  SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\n#  CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\n#  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n#  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\nfrom typing import Optional\n\nimport grpc\nfrom grpc._interceptor import _ClientCallDetails\n\nimport elasticapm\nfrom elasticapm.conf import constants\nfrom elasticapm.traces import Span\nfrom elasticapm.utils import default_ports\n\n\nclass _ClientInterceptor(\n    grpc.UnaryUnaryClientInterceptor,\n    # grpc.UnaryStreamClientInterceptor,\n    # grpc.StreamUnaryClientInterceptor,\n    # grpc.StreamStreamClientInterceptor,\n):\n    def __init__(self, host: Optional[str], port: Optional[str], secure: bool):\n        self.host: str = host\n        self.port: str = port\n        self.secure: bool = secure\n        schema = \"https\" if secure else \"http\"\n        resource = f\"{schema}://{host}\"\n        if port and int(port) != default_ports[schema]:\n            resource += f\":{port}\"\n\n        self._context = {\n            \"http\": {\n                \"url\": resource,\n            },\n            \"destination\": {\n                \"address\": host,\n                \"port\": port,\n            },\n        }\n\n    def intercept_unary_unary(self, continuation, client_call_details, request):\n        \"\"\"Intercepts a unary-unary invocation asynchronously.\n\n        Args:\n          continuation: A function that proceeds with the invocation by\n            executing the next interceptor in chain or invoking the\n            actual RPC on the underlying Channel. It is the interceptor's\n            responsibility to call it if it decides to move the RPC forward.\n            The interceptor can use\n            `response_future = continuation(client_call_details, request)`\n            to continue with the RPC. `continuation` returns an object that is\n            both a Call for the RPC and a Future. In the event of RPC\n            completion, the return Call-Future's result value will be\n            the response message of the RPC. Should the event terminate\n            with non-OK status, the returned Call-Future's exception value\n            will be an RpcError.\n          client_call_details: A ClientCallDetails object describing the\n            outgoing RPC.\n          request: The request value for the RPC.\n\n        Returns:\n            An object that is both a Call for the RPC and a Future.\n            In the event of RPC completion, the return Call-Future's\n            result value will be the response message of the RPC.\n            Should the event terminate with non-OK status, the returned\n            Call-Future's exception value will be an RpcError.\n        \"\"\"\n        with elasticapm.capture_span(\n            client_call_details.method, span_type=\"external\", span_subtype=\"grpc\", extra=self._context.copy(), leaf=True\n        ) as span:\n            if not span:\n                return continuation(client_call_details, request)\n            client_call_details = self.attach_traceparent(client_call_details, span)\n            try:\n                response = continuation(client_call_details, request)\n            except grpc.RpcError:\n                span.set_failure()\n                raise\n\n            return response\n\n    # TODO: instrument other types of requests once the spec is ready\n\n    # def intercept_unary_stream(self, continuation, client_call_details,\n    #                            request):\n    #     \"\"\"Intercepts a unary-stream invocation.\n    #\n    #     Args:\n    #       continuation: A function that proceeds with the invocation by\n    #         executing the next interceptor in chain or invoking the\n    #         actual RPC on the underlying Channel. It is the interceptor's\n    #         responsibility to call it if it decides to move the RPC forward.\n    #         The interceptor can use\n    #         `response_iterator = continuation(client_call_details, request)`\n    #         to continue with the RPC. `continuation` returns an object that is\n    #         both a Call for the RPC and an iterator for response values.\n    #         Drawing response values from the returned Call-iterator may\n    #         raise RpcError indicating termination of the RPC with non-OK\n    #         status.\n    #       client_call_details: A ClientCallDetails object describing the\n    #         outgoing RPC.\n    #       request: The request value for the RPC.\n    #\n    #     Returns:\n    #         An object that is both a Call for the RPC and an iterator of\n    #         response values. Drawing response values from the returned\n    #         Call-iterator may raise RpcError indicating termination of\n    #         the RPC with non-OK status. This object *should* also fulfill the\n    #         Future interface, though it may not.\n    #     \"\"\"\n    #     response_iterator = continuation(client_call_details, request)\n    #     return response_iterator\n    #\n    # def intercept_stream_unary(self, continuation, client_call_details,\n    #                            request_iterator):\n    #     \"\"\"Intercepts a stream-unary invocation asynchronously.\n    #\n    #     Args:\n    #       continuation: A function that proceeds with the invocation by\n    #         executing the next interceptor in chain or invoking the\n    #         actual RPC on the underlying Channel. It is the interceptor's\n    #         responsibility to call it if it decides to move the RPC forward.\n    #         The interceptor can use\n    #         `response_future = continuation(client_call_details, request_iterator)`\n    #         to continue with the RPC. `continuation` returns an object that is\n    #         both a Call for the RPC and a Future. In the event of RPC completion,\n    #         the return Call-Future's result value will be the response message\n    #         of the RPC. Should the event terminate with non-OK status, the\n    #         returned Call-Future's exception value will be an RpcError.\n    #       client_call_details: A ClientCallDetails object describing the\n    #         outgoing RPC.\n    #       request_iterator: An iterator that yields request values for the RPC.\n    #\n    #     Returns:\n    #       An object that is both a Call for the RPC and a Future.\n    #       In the event of RPC completion, the return Call-Future's\n    #       result value will be the response message of the RPC.\n    #       Should the event terminate with non-OK status, the returned\n    #       Call-Future's exception value will be an RpcError.\n    #     \"\"\"\n    #\n    # def intercept_stream_stream(self, continuation, client_call_details,\n    #                             request_iterator):\n    #     \"\"\"Intercepts a stream-stream invocation.\n    #\n    #     Args:\n    #       continuation: A function that proceeds with the invocation by\n    #         executing the next interceptor in chain or invoking the\n    #         actual RPC on the underlying Channel. It is the interceptor's\n    #         responsibility to call it if it decides to move the RPC forward.\n    #         The interceptor can use\n    #         `response_iterator = continuation(client_call_details, request_iterator)`\n    #         to continue with the RPC. `continuation` returns an object that is\n    #         both a Call for the RPC and an iterator for response values.\n    #         Drawing response values from the returned Call-iterator may\n    #         raise RpcError indicating termination of the RPC with non-OK\n    #         status.\n    #       client_call_details: A ClientCallDetails object describing the\n    #         outgoing RPC.\n    #       request_iterator: An iterator that yields request values for the RPC.\n    #\n    #     Returns:\n    #       An object that is both a Call for the RPC and an iterator of\n    #       response values. Drawing response values from the returned\n    #       Call-iterator may raise RpcError indicating termination of\n    #       the RPC with non-OK status. This object *should* also fulfill the\n    #       Future interface, though it may not.\n    #     \"\"\"\n\n    def attach_traceparent(self, client_call_details: _ClientCallDetails, span: Span):\n        if not span.transaction:\n            return client_call_details\n        meta = list(client_call_details.metadata) if client_call_details.metadata else []\n        if constants.TRACEPARENT_HEADER_NAME not in meta:\n            traceparent = span.transaction.trace_parent.copy_from(span_id=span.id)\n            meta.extend(\n                (\n                    (constants.TRACEPARENT_HEADER_NAME, traceparent.to_string()),\n                    (constants.TRACESTATE_HEADER_NAME, traceparent.tracestate),\n                )\n            )\n        return client_call_details._replace(metadata=meta)\n", "path": "elasticapm/contrib/grpc/client_interceptor.py"}], "after_files": [{"content": "#  BSD 3-Clause License\n#\n#  Copyright (c) 2022, Elasticsearch BV\n#  All rights reserved.\n#\n#  Redistribution and use in source and binary forms, with or without\n#  modification, are permitted provided that the following conditions are met:\n#\n#  * Redistributions of source code must retain the above copyright notice, this\n#    list of conditions and the following disclaimer.\n#\n#  * Redistributions in binary form must reproduce the above copyright notice,\n#    this list of conditions and the following disclaimer in the documentation\n#    and/or other materials provided with the distribution.\n#\n#  * Neither the name of the copyright holder nor the names of its\n#    contributors may be used to endorse or promote products derived from\n#    this software without specific prior written permission.\n#\n#  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\n#  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\n#  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\n#  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\n#  FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\n#  DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\n#  SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\n#  CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\n#  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n#  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\nfrom typing import Optional\n\nimport grpc\nfrom grpc._interceptor import _ClientCallDetails\n\nimport elasticapm\nfrom elasticapm.conf import constants\nfrom elasticapm.traces import DroppedSpan, SpanType\nfrom elasticapm.utils import default_ports\n\n\nclass _ClientInterceptor(\n    grpc.UnaryUnaryClientInterceptor,\n    # grpc.UnaryStreamClientInterceptor,\n    # grpc.StreamUnaryClientInterceptor,\n    # grpc.StreamStreamClientInterceptor,\n):\n    def __init__(self, host: Optional[str], port: Optional[str], secure: bool):\n        self.host: str = host\n        self.port: str = port\n        self.secure: bool = secure\n        schema = \"https\" if secure else \"http\"\n        resource = f\"{schema}://{host}\"\n        if port and int(port) != default_ports[schema]:\n            resource += f\":{port}\"\n\n        self._context = {\n            \"http\": {\n                \"url\": resource,\n            },\n            \"destination\": {\n                \"address\": host,\n                \"port\": port,\n            },\n        }\n\n    def intercept_unary_unary(self, continuation, client_call_details, request):\n        \"\"\"Intercepts a unary-unary invocation asynchronously.\n\n        Args:\n          continuation: A function that proceeds with the invocation by\n            executing the next interceptor in chain or invoking the\n            actual RPC on the underlying Channel. It is the interceptor's\n            responsibility to call it if it decides to move the RPC forward.\n            The interceptor can use\n            `response_future = continuation(client_call_details, request)`\n            to continue with the RPC. `continuation` returns an object that is\n            both a Call for the RPC and a Future. In the event of RPC\n            completion, the return Call-Future's result value will be\n            the response message of the RPC. Should the event terminate\n            with non-OK status, the returned Call-Future's exception value\n            will be an RpcError.\n          client_call_details: A ClientCallDetails object describing the\n            outgoing RPC.\n          request: The request value for the RPC.\n\n        Returns:\n            An object that is both a Call for the RPC and a Future.\n            In the event of RPC completion, the return Call-Future's\n            result value will be the response message of the RPC.\n            Should the event terminate with non-OK status, the returned\n            Call-Future's exception value will be an RpcError.\n        \"\"\"\n        with elasticapm.capture_span(\n            client_call_details.method, span_type=\"external\", span_subtype=\"grpc\", extra=self._context.copy(), leaf=True\n        ) as span:\n            if not span:\n                return continuation(client_call_details, request)\n            client_call_details = self.attach_traceparent(client_call_details, span)\n            try:\n                response = continuation(client_call_details, request)\n            except grpc.RpcError:\n                span.set_failure()\n                raise\n\n            return response\n\n    # TODO: instrument other types of requests once the spec is ready\n\n    # def intercept_unary_stream(self, continuation, client_call_details,\n    #                            request):\n    #     \"\"\"Intercepts a unary-stream invocation.\n    #\n    #     Args:\n    #       continuation: A function that proceeds with the invocation by\n    #         executing the next interceptor in chain or invoking the\n    #         actual RPC on the underlying Channel. It is the interceptor's\n    #         responsibility to call it if it decides to move the RPC forward.\n    #         The interceptor can use\n    #         `response_iterator = continuation(client_call_details, request)`\n    #         to continue with the RPC. `continuation` returns an object that is\n    #         both a Call for the RPC and an iterator for response values.\n    #         Drawing response values from the returned Call-iterator may\n    #         raise RpcError indicating termination of the RPC with non-OK\n    #         status.\n    #       client_call_details: A ClientCallDetails object describing the\n    #         outgoing RPC.\n    #       request: The request value for the RPC.\n    #\n    #     Returns:\n    #         An object that is both a Call for the RPC and an iterator of\n    #         response values. Drawing response values from the returned\n    #         Call-iterator may raise RpcError indicating termination of\n    #         the RPC with non-OK status. This object *should* also fulfill the\n    #         Future interface, though it may not.\n    #     \"\"\"\n    #     response_iterator = continuation(client_call_details, request)\n    #     return response_iterator\n    #\n    # def intercept_stream_unary(self, continuation, client_call_details,\n    #                            request_iterator):\n    #     \"\"\"Intercepts a stream-unary invocation asynchronously.\n    #\n    #     Args:\n    #       continuation: A function that proceeds with the invocation by\n    #         executing the next interceptor in chain or invoking the\n    #         actual RPC on the underlying Channel. It is the interceptor's\n    #         responsibility to call it if it decides to move the RPC forward.\n    #         The interceptor can use\n    #         `response_future = continuation(client_call_details, request_iterator)`\n    #         to continue with the RPC. `continuation` returns an object that is\n    #         both a Call for the RPC and a Future. In the event of RPC completion,\n    #         the return Call-Future's result value will be the response message\n    #         of the RPC. Should the event terminate with non-OK status, the\n    #         returned Call-Future's exception value will be an RpcError.\n    #       client_call_details: A ClientCallDetails object describing the\n    #         outgoing RPC.\n    #       request_iterator: An iterator that yields request values for the RPC.\n    #\n    #     Returns:\n    #       An object that is both a Call for the RPC and a Future.\n    #       In the event of RPC completion, the return Call-Future's\n    #       result value will be the response message of the RPC.\n    #       Should the event terminate with non-OK status, the returned\n    #       Call-Future's exception value will be an RpcError.\n    #     \"\"\"\n    #\n    # def intercept_stream_stream(self, continuation, client_call_details,\n    #                             request_iterator):\n    #     \"\"\"Intercepts a stream-stream invocation.\n    #\n    #     Args:\n    #       continuation: A function that proceeds with the invocation by\n    #         executing the next interceptor in chain or invoking the\n    #         actual RPC on the underlying Channel. It is the interceptor's\n    #         responsibility to call it if it decides to move the RPC forward.\n    #         The interceptor can use\n    #         `response_iterator = continuation(client_call_details, request_iterator)`\n    #         to continue with the RPC. `continuation` returns an object that is\n    #         both a Call for the RPC and an iterator for response values.\n    #         Drawing response values from the returned Call-iterator may\n    #         raise RpcError indicating termination of the RPC with non-OK\n    #         status.\n    #       client_call_details: A ClientCallDetails object describing the\n    #         outgoing RPC.\n    #       request_iterator: An iterator that yields request values for the RPC.\n    #\n    #     Returns:\n    #       An object that is both a Call for the RPC and an iterator of\n    #       response values. Drawing response values from the returned\n    #       Call-iterator may raise RpcError indicating termination of\n    #       the RPC with non-OK status. This object *should* also fulfill the\n    #       Future interface, though it may not.\n    #     \"\"\"\n\n    def attach_traceparent(self, client_call_details: _ClientCallDetails, span: SpanType) -> _ClientCallDetails:\n        if isinstance(span, DroppedSpan) or not span.transaction:\n            return client_call_details\n        meta = list(client_call_details.metadata) if client_call_details.metadata else []\n        if constants.TRACEPARENT_HEADER_NAME not in meta:\n            traceparent = span.transaction.trace_parent.copy_from(span_id=span.id)\n            meta.extend(\n                (\n                    (constants.TRACEPARENT_HEADER_NAME, traceparent.to_string()),\n                    (constants.TRACESTATE_HEADER_NAME, traceparent.tracestate),\n                )\n            )\n        return client_call_details._replace(metadata=meta)\n", "path": "elasticapm/contrib/grpc/client_interceptor.py"}]}