We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
[GRPC] Handle the null span case
**Describe the bug**: ...

**To Reproduce**

1. Setup ElasticAPM in Django
2. Integrate with Google PubSub client
3. Try to publish message from your script
4. Stacktrace:
```
Exception in thread Thread-CommitBatchPublisher:
Traceback (most recent call last):
  File "/usr/local/lib/python3.8/threading.py", line 932, in _bootstrap_inner
    self.run()
  File "/usr/local/lib/python3.8/threading.py", line 870, in run
    self._target(*self._args, **self._kwargs)
  File "/usr/local/lib/python3.8/site-packages/google/cloud/pubsub_v1/publisher/_batch/thread.py", line 274, in _commit
    response = self._client._gapic_publish(
  File "/usr/local/lib/python3.8/site-packages/google/cloud/pubsub_v1/publisher/client.py", line 272, in _gapic_publish
    return super().publish(*args, **kwargs)
  File "/usr/local/lib/python3.8/site-packages/google/pubsub_v1/services/publisher/client.py", line 781, in publish
    response = rpc(
  File "/usr/local/lib/python3.8/site-packages/google/api_core/gapic_v1/method.py", line 113, in __call__
    return wrapped_func(*args, **kwargs)
  File "/usr/local/lib/python3.8/site-packages/google/api_core/retry.py", line 349, in retry_wrapped_func
    return retry_target(
  File "/usr/local/lib/python3.8/site-packages/google/api_core/retry.py", line 191, in retry_target
    return target()
  File "/usr/local/lib/python3.8/site-packages/google/api_core/timeout.py", line 120, in func_with_timeout
    return func(*args, **kwargs)
  File "/usr/local/lib/python3.8/site-packages/google/api_core/grpc_helpers.py", line 72, in error_remapped_callable
    return callable_(*args, **kwargs)
  File "/usr/local/lib/python3.8/site-packages/grpc/_interceptor.py", line 247, in __call__
    response, ignored_call = self._with_call(request,
  File "/usr/local/lib/python3.8/site-packages/grpc/_interceptor.py", line 287, in _with_call
    call = self._interceptor.intercept_unary_unary(continuation,
  File "/usr/local/lib/python3.8/site-packages/elasticapm/contrib/grpc/client_interceptor.py", line 97, in intercept_unary_unary
    client_call_details = self.attach_traceparent(client_call_details, span)
  File "/usr/local/lib/python3.8/site-packages/elasticapm/contrib/grpc/client_interceptor.py", line 195, in attach_traceparent
    if not span.transaction:
AttributeError: 'NoneType' object has no attribute 'transaction'
```

**Environment (please complete the following information)**
- OS: Linux
- Python version: 3.8
- Framework and version [e.g. Django 2.1]: Django 3.8
- APM Server version: 7.15
- Agent version: 6.14.0


**Additional context**

Add any other context about the problem here.
- `requirements.txt`:
  <details>
    <summary>Click to expand</summary>

    ```
    google-cloud-pubsub==2.13.11
    ```
  </details>

**Initial digging**
Looks like the span is not being returned from the context manager, but the grpc code is not handling the `None` case. 
* [context manager](https://github.com/elastic/apm-agent-python/blob/main/elasticapm/traces.py#L1078)
* [grpc code](https://github.com/elastic/apm-agent-python/blob/main/elasticapm/contrib/grpc/client_interceptor.py#L195)

I can make a patch to handle the `None` case (and just return the client call details), but not sure if that is the right call here. Any guidance would be appreciated!


--- 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             client_call_details = self.attach_traceparent(client_call_details, span)
98             try:
99                 response = continuation(client_call_details, request)
100             except grpc.RpcError:
101                 span.set_failure()
102                 raise
103 
104             return response
105 
106     # TODO: instrument other types of requests once the spec is ready
107 
108     # def intercept_unary_stream(self, continuation, client_call_details,
109     #                            request):
110     #     """Intercepts a unary-stream invocation.
111     #
112     #     Args:
113     #       continuation: A function that proceeds with the invocation by
114     #         executing the next interceptor in chain or invoking the
115     #         actual RPC on the underlying Channel. It is the interceptor's
116     #         responsibility to call it if it decides to move the RPC forward.
117     #         The interceptor can use
118     #         `response_iterator = continuation(client_call_details, request)`
119     #         to continue with the RPC. `continuation` returns an object that is
120     #         both a Call for the RPC and an iterator for response values.
121     #         Drawing response values from the returned Call-iterator may
122     #         raise RpcError indicating termination of the RPC with non-OK
123     #         status.
124     #       client_call_details: A ClientCallDetails object describing the
125     #         outgoing RPC.
126     #       request: The request value for the RPC.
127     #
128     #     Returns:
129     #         An object that is both a Call for the RPC and an iterator of
130     #         response values. Drawing response values from the returned
131     #         Call-iterator may raise RpcError indicating termination of
132     #         the RPC with non-OK status. This object *should* also fulfill the
133     #         Future interface, though it may not.
134     #     """
135     #     response_iterator = continuation(client_call_details, request)
136     #     return response_iterator
137     #
138     # def intercept_stream_unary(self, continuation, client_call_details,
139     #                            request_iterator):
140     #     """Intercepts a stream-unary invocation asynchronously.
141     #
142     #     Args:
143     #       continuation: A function that proceeds with the invocation by
144     #         executing the next interceptor in chain or invoking the
145     #         actual RPC on the underlying Channel. It is the interceptor's
146     #         responsibility to call it if it decides to move the RPC forward.
147     #         The interceptor can use
148     #         `response_future = continuation(client_call_details, request_iterator)`
149     #         to continue with the RPC. `continuation` returns an object that is
150     #         both a Call for the RPC and a Future. In the event of RPC completion,
151     #         the return Call-Future's result value will be the response message
152     #         of the RPC. Should the event terminate with non-OK status, the
153     #         returned Call-Future's exception value will be an RpcError.
154     #       client_call_details: A ClientCallDetails object describing the
155     #         outgoing RPC.
156     #       request_iterator: An iterator that yields request values for the RPC.
157     #
158     #     Returns:
159     #       An object that is both a Call for the RPC and a Future.
160     #       In the event of RPC completion, the return Call-Future's
161     #       result value will be the response message of the RPC.
162     #       Should the event terminate with non-OK status, the returned
163     #       Call-Future's exception value will be an RpcError.
164     #     """
165     #
166     # def intercept_stream_stream(self, continuation, client_call_details,
167     #                             request_iterator):
168     #     """Intercepts a stream-stream invocation.
169     #
170     #     Args:
171     #       continuation: A function that proceeds with the invocation by
172     #         executing the next interceptor in chain or invoking the
173     #         actual RPC on the underlying Channel. It is the interceptor's
174     #         responsibility to call it if it decides to move the RPC forward.
175     #         The interceptor can use
176     #         `response_iterator = continuation(client_call_details, request_iterator)`
177     #         to continue with the RPC. `continuation` returns an object that is
178     #         both a Call for the RPC and an iterator for response values.
179     #         Drawing response values from the returned Call-iterator may
180     #         raise RpcError indicating termination of the RPC with non-OK
181     #         status.
182     #       client_call_details: A ClientCallDetails object describing the
183     #         outgoing RPC.
184     #       request_iterator: An iterator that yields request values for the RPC.
185     #
186     #     Returns:
187     #       An object that is both a Call for the RPC and an iterator of
188     #       response values. Drawing response values from the returned
189     #       Call-iterator may raise RpcError indicating termination of
190     #       the RPC with non-OK status. This object *should* also fulfill the
191     #       Future interface, though it may not.
192     #     """
193 
194     def attach_traceparent(self, client_call_details: _ClientCallDetails, span: Span):
195         if not span.transaction:
196             return client_call_details
197         meta = list(client_call_details.metadata) if client_call_details.metadata else []
198         if constants.TRACEPARENT_HEADER_NAME not in meta:
199             traceparent = span.transaction.trace_parent.copy_from(span_id=span.id)
200             meta.extend(
201                 (
202                     (constants.TRACEPARENT_HEADER_NAME, traceparent.to_string()),
203                     (constants.TRACESTATE_HEADER_NAME, traceparent.tracestate),
204                 )
205             )
206         return client_call_details._replace(metadata=meta)
207
```
--- 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
@@ -94,6 +94,8 @@
         with elasticapm.capture_span(
             client_call_details.method, span_type="external", span_subtype="grpc", extra=self._context.copy(), leaf=True
         ) as span:
+            if not span:
+                return continuation(client_call_details, request)
             client_call_details = self.attach_traceparent(client_call_details, span)
             try:
                 response = continuation(client_call_details, request)

{"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@@ -94,6 +94,8 @@\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", "issue": "[GRPC] Handle the null span case\n**Describe the bug**: ...\r\n\r\n**To Reproduce**\r\n\r\n1. Setup ElasticAPM in Django\r\n2. Integrate with Google PubSub client\r\n3. Try to publish message from your script\r\n4. Stacktrace:\r\n```\r\nException in thread Thread-CommitBatchPublisher:\r\nTraceback (most recent call last):\r\n  File \"/usr/local/lib/python3.8/threading.py\", line 932, in _bootstrap_inner\r\n    self.run()\r\n  File \"/usr/local/lib/python3.8/threading.py\", line 870, in run\r\n    self._target(*self._args, **self._kwargs)\r\n  File \"/usr/local/lib/python3.8/site-packages/google/cloud/pubsub_v1/publisher/_batch/thread.py\", line 274, in _commit\r\n    response = self._client._gapic_publish(\r\n  File \"/usr/local/lib/python3.8/site-packages/google/cloud/pubsub_v1/publisher/client.py\", line 272, in _gapic_publish\r\n    return super().publish(*args, **kwargs)\r\n  File \"/usr/local/lib/python3.8/site-packages/google/pubsub_v1/services/publisher/client.py\", line 781, in publish\r\n    response = rpc(\r\n  File \"/usr/local/lib/python3.8/site-packages/google/api_core/gapic_v1/method.py\", line 113, in __call__\r\n    return wrapped_func(*args, **kwargs)\r\n  File \"/usr/local/lib/python3.8/site-packages/google/api_core/retry.py\", line 349, in retry_wrapped_func\r\n    return retry_target(\r\n  File \"/usr/local/lib/python3.8/site-packages/google/api_core/retry.py\", line 191, in retry_target\r\n    return target()\r\n  File \"/usr/local/lib/python3.8/site-packages/google/api_core/timeout.py\", line 120, in func_with_timeout\r\n    return func(*args, **kwargs)\r\n  File \"/usr/local/lib/python3.8/site-packages/google/api_core/grpc_helpers.py\", line 72, in error_remapped_callable\r\n    return callable_(*args, **kwargs)\r\n  File \"/usr/local/lib/python3.8/site-packages/grpc/_interceptor.py\", line 247, in __call__\r\n    response, ignored_call = self._with_call(request,\r\n  File \"/usr/local/lib/python3.8/site-packages/grpc/_interceptor.py\", line 287, in _with_call\r\n    call = self._interceptor.intercept_unary_unary(continuation,\r\n  File \"/usr/local/lib/python3.8/site-packages/elasticapm/contrib/grpc/client_interceptor.py\", line 97, in intercept_unary_unary\r\n    client_call_details = self.attach_traceparent(client_call_details, span)\r\n  File \"/usr/local/lib/python3.8/site-packages/elasticapm/contrib/grpc/client_interceptor.py\", line 195, in attach_traceparent\r\n    if not span.transaction:\r\nAttributeError: 'NoneType' object has no attribute 'transaction'\r\n```\r\n\r\n**Environment (please complete the following information)**\r\n- OS: Linux\r\n- Python version: 3.8\r\n- Framework and version [e.g. Django 2.1]: Django 3.8\r\n- APM Server version: 7.15\r\n- Agent version: 6.14.0\r\n\r\n\r\n**Additional context**\r\n\r\nAdd any other context about the problem here.\r\n- `requirements.txt`:\r\n  <details>\r\n    <summary>Click to expand</summary>\r\n\r\n    ```\r\n    google-cloud-pubsub==2.13.11\r\n    ```\r\n  </details>\r\n\r\n**Initial digging**\r\nLooks like the span is not being returned from the context manager, but the grpc code is not handling the `None` case. \r\n* [context manager](https://github.com/elastic/apm-agent-python/blob/main/elasticapm/traces.py#L1078)\r\n* [grpc code](https://github.com/elastic/apm-agent-python/blob/main/elasticapm/contrib/grpc/client_interceptor.py#L195)\r\n\r\nI can make a patch to handle the `None` case (and just return the client call details), but not sure if that is the right call here. Any guidance would be appreciated!\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            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 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"}]}