We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
AttributeError: 'Retry' object has no attribute 'copy'
The `elasticapm` Python client fails with `AttributeError: 'Retry' object has no attribute 'copy'` during HTTP retries.

The Elastic APM UI points towards the following lines being responsible for the crash https://github.com/elastic/apm-agent-python/blob/c9a1f7a74b4b8e39af06f9b383037a3ae49ca42b/elasticapm/instrumentation/packages/urllib3.py#L66-L68 

**To Reproduce**

Configure a `requests` session with [urllib3 retries](https://urllib3.readthedocs.io/en/stable/reference/urllib3.util.html) as described below. Then point it at a flaky endpoint.

``` python
class TimeoutHTTPAdapter(HTTPAdapter):
    def __init__(self, retries: int, *args: Any, **kwargs: Any) -> None:
        self.timeout = kwargs.pop("timeout", 30)
        self._retries = retries
        super().__init__(*args, **kwargs)

    def send( 
        self, request: requests.PreparedRequest, **kwargs: Any
    ) -> requests.Response:
        kwargs.setdefault("timeout", self.timeout)
        return super().send(request, **kwargs)

def session_with_retries(
    retries: int = 3,
    backoff_factor: float = 1,
    timeout: int = 30,
    status_forcelist: Collection[int] = (408, 429, 500, 502, 503, 504),
) -> requests.Session:

    retry_strategy = Retry(
        total=retries,
        backoff_factor=backoff_factor,
        status_forcelist=status_forcelist,
        allowed_methods=Retry.DEFAULT_ALLOWED_METHODS.union(["POST", "PATCH"]),
    )
    adapter = TimeoutHTTPAdapter(
        retries=retries, max_retries=retry_strategy, timeout=timeout
    )

    s = requests.Session()
    s.mount("http://", adapter)
    s.mount("https://", adapter)
    return s
```

**Environment**
- OS: Linux
- Python version: Python 3.11
- Framework and version: `urllib3==2.0.6` and `requests==2.31.0`
- APM Server version: 8.10.4
- Agent version: `elastic-apm==6.19.0`

**Background**
* This appears to be related to the recent urllib3 fixes https://github.com/elastic/apm-agent-python/pull/1822 and https://github.com/elastic/apm-agent-python/issues/1816 
AttributeError: 'Retry' object has no attribute 'copy'
The `elasticapm` Python client fails with `AttributeError: 'Retry' object has no attribute 'copy'` during HTTP retries.

The Elastic APM UI points towards the following lines being responsible for the crash https://github.com/elastic/apm-agent-python/blob/c9a1f7a74b4b8e39af06f9b383037a3ae49ca42b/elasticapm/instrumentation/packages/urllib3.py#L66-L68 

**To Reproduce**

Configure a `requests` session with [urllib3 retries](https://urllib3.readthedocs.io/en/stable/reference/urllib3.util.html) as described below. Then point it at a flaky endpoint.

``` python
class TimeoutHTTPAdapter(HTTPAdapter):
    def __init__(self, retries: int, *args: Any, **kwargs: Any) -> None:
        self.timeout = kwargs.pop("timeout", 30)
        self._retries = retries
        super().__init__(*args, **kwargs)

    def send( 
        self, request: requests.PreparedRequest, **kwargs: Any
    ) -> requests.Response:
        kwargs.setdefault("timeout", self.timeout)
        return super().send(request, **kwargs)

def session_with_retries(
    retries: int = 3,
    backoff_factor: float = 1,
    timeout: int = 30,
    status_forcelist: Collection[int] = (408, 429, 500, 502, 503, 504),
) -> requests.Session:

    retry_strategy = Retry(
        total=retries,
        backoff_factor=backoff_factor,
        status_forcelist=status_forcelist,
        allowed_methods=Retry.DEFAULT_ALLOWED_METHODS.union(["POST", "PATCH"]),
    )
    adapter = TimeoutHTTPAdapter(
        retries=retries, max_retries=retry_strategy, timeout=timeout
    )

    s = requests.Session()
    s.mount("http://", adapter)
    s.mount("https://", adapter)
    return s
```

**Environment**
- OS: Linux
- Python version: Python 3.11
- Framework and version: `urllib3==2.0.6` and `requests==2.31.0`
- APM Server version: 8.10.4
- Agent version: `elastic-apm==6.19.0`

**Background**
* This appears to be related to the recent urllib3 fixes https://github.com/elastic/apm-agent-python/pull/1822 and https://github.com/elastic/apm-agent-python/issues/1816 

--- 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/instrumentation/packages/urllib3.py`
Content:
```
1 #  BSD 3-Clause License
2 #
3 #  Copyright (c) 2019, Elasticsearch BV
4 #  All rights reserved.
5 #
6 #  Redistribution and use in source and binary forms, with or without
7 #  modification, are permitted provided that the following conditions are met:
8 #
9 #  * Redistributions of source code must retain the above copyright notice, this
10 #    list of conditions and the following disclaimer.
11 #
12 #  * Redistributions in binary form must reproduce the above copyright notice,
13 #    this list of conditions and the following disclaimer in the documentation
14 #    and/or other materials provided with the distribution.
15 #
16 #  * Neither the name of the copyright holder nor the names of its
17 #    contributors may be used to endorse or promote products derived from
18 #    this software without specific prior written permission.
19 #
20 #  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21 #  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22 #  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23 #  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24 #  FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25 #  DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26 #  SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27 #  CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28 #  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 #  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30 
31 import itertools
32 
33 from elasticapm.conf import constants
34 from elasticapm.instrumentation.packages.base import AbstractInstrumentedModule
35 from elasticapm.traces import DroppedSpan, capture_span, execution_context
36 from elasticapm.utils import default_ports
37 from elasticapm.utils.disttracing import TracingOptions
38 
39 
40 def _set_disttracing_headers(headers, trace_parent, transaction) -> None:
41     trace_parent_str = trace_parent.to_string()
42     headers[constants.TRACEPARENT_HEADER_NAME] = trace_parent_str
43     if transaction.tracer.config.use_elastic_traceparent_header:
44         headers[constants.TRACEPARENT_LEGACY_HEADER_NAME] = trace_parent_str
45     if trace_parent.tracestate:
46         headers[constants.TRACESTATE_HEADER_NAME] = trace_parent.tracestate
47 
48 
49 def update_headers(args, kwargs, instance, transaction, trace_parent):
50     """
51     The headers might be in 3 different places: as 4th positional argument, as "headers" keyword argument,
52     or, if none of the former two are provided, as instance variable on the HTTPConnection object.
53 
54     If the headers are in the positional arguments tuple, a new tuple with updated headers will be returned.
55     If they are in the keyword arguments or on the instance, an updated kwargs dict will be returned
56 
57     :param args: list of positional arguments
58     :param kwargs: dict of keyword arguments
59     :param instance: the HTTPConnection instance
60     :param transaction: the Transaction object
61     :param trace_parent: the TraceParent object
62     :return: an (args, kwargs) tuple
63     """
64     from urllib3._version import __version__ as urllib3_version
65 
66     if urllib3_version.startswith("2") and len(args) >= 5 and args[4]:
67         headers = args[4].copy()
68         args = tuple(itertools.chain((args[:4]), (headers,), args[5:]))
69     elif len(args) >= 4 and args[3]:
70         headers = args[3].copy()
71         args = tuple(itertools.chain((args[:3]), (headers,), args[4:]))
72     elif "headers" in kwargs and kwargs["headers"]:
73         headers = kwargs["headers"].copy()
74         kwargs["headers"] = headers
75     else:
76         headers = instance.headers.copy() if instance.headers else {}
77         # we don't want to change the instance headers, so we'll cheat and
78         # set the headers as keywords. This slightly changes how the wrapped
79         # method is called compared to uninstrumented code.
80         kwargs["headers"] = headers
81     _set_disttracing_headers(headers, trace_parent, transaction)
82     return args, kwargs
83 
84 
85 class Urllib3Instrumentation(AbstractInstrumentedModule):
86     name = "urllib3"
87 
88     instrument_list = [
89         ("urllib3.connectionpool", "HTTPConnectionPool.urlopen"),
90         # packages that vendor or vendored urllib3 in the past
91         ("requests.packages.urllib3.connectionpool", "HTTPConnectionPool.urlopen"),
92         ("botocore.vendored.requests.packages.urllib3.connectionpool", "HTTPConnectionPool.urlopen"),
93     ]
94 
95     def call(self, module, method, wrapped, instance, args, kwargs):
96         if "method" in kwargs:
97             method = kwargs["method"]
98         else:
99             method = args[0]
100 
101         host = instance.host
102 
103         if instance.port != default_ports.get(instance.scheme):
104             host += ":" + str(instance.port)
105 
106         if "url" in kwargs:
107             url = kwargs["url"]
108         else:
109             url = args[1]
110 
111         signature = method.upper() + " " + host
112 
113         if url.startswith("/"):
114             url = "%s://%s%s" % (instance.scheme, host, url)
115 
116         transaction = execution_context.get_transaction()
117 
118         with capture_span(
119             signature,
120             span_type="external",
121             span_subtype="http",
122             extra={"http": {"url": url}},
123             leaf=True,
124         ) as span:
125             # if urllib3 has been called in a leaf span, this span might be a DroppedSpan.
126             leaf_span = span
127             while isinstance(leaf_span, DroppedSpan):
128                 leaf_span = leaf_span.parent
129 
130             parent_id = leaf_span.id if leaf_span else transaction.id
131             trace_parent = transaction.trace_parent.copy_from(
132                 span_id=parent_id, trace_options=TracingOptions(recorded=True)
133             )
134             args, kwargs = update_headers(args, kwargs, instance, transaction, trace_parent)
135             if leaf_span:
136                 leaf_span.dist_tracing_propagated = True
137             response = wrapped(*args, **kwargs)
138             if response:
139                 if span.context:
140                     span.context["http"]["status_code"] = response.status
141                 span.set_success() if response.status < 400 else span.set_failure()
142             return response
143 
144     def mutate_unsampled_call_args(self, module, method, wrapped, instance, args, kwargs, transaction):
145         # since we don't have a span, we set the span id to the transaction id
146         trace_parent = transaction.trace_parent.copy_from(
147             span_id=transaction.id, trace_options=TracingOptions(recorded=False)
148         )
149         return update_headers(args, kwargs, instance, transaction, trace_parent)
150
```
--- 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/instrumentation/packages/urllib3.py b/elasticapm/instrumentation/packages/urllib3.py
--- a/elasticapm/instrumentation/packages/urllib3.py
+++ b/elasticapm/instrumentation/packages/urllib3.py
@@ -61,12 +61,7 @@
     :param trace_parent: the TraceParent object
     :return: an (args, kwargs) tuple
     """
-    from urllib3._version import __version__ as urllib3_version
-
-    if urllib3_version.startswith("2") and len(args) >= 5 and args[4]:
-        headers = args[4].copy()
-        args = tuple(itertools.chain((args[:4]), (headers,), args[5:]))
-    elif len(args) >= 4 and args[3]:
+    if len(args) >= 4 and args[3]:
         headers = args[3].copy()
         args = tuple(itertools.chain((args[:3]), (headers,), args[4:]))
     elif "headers" in kwargs and kwargs["headers"]:

{"golden_diff": "diff --git a/elasticapm/instrumentation/packages/urllib3.py b/elasticapm/instrumentation/packages/urllib3.py\n--- a/elasticapm/instrumentation/packages/urllib3.py\n+++ b/elasticapm/instrumentation/packages/urllib3.py\n@@ -61,12 +61,7 @@\n     :param trace_parent: the TraceParent object\n     :return: an (args, kwargs) tuple\n     \"\"\"\n-    from urllib3._version import __version__ as urllib3_version\n-\n-    if urllib3_version.startswith(\"2\") and len(args) >= 5 and args[4]:\n-        headers = args[4].copy()\n-        args = tuple(itertools.chain((args[:4]), (headers,), args[5:]))\n-    elif len(args) >= 4 and args[3]:\n+    if len(args) >= 4 and args[3]:\n         headers = args[3].copy()\n         args = tuple(itertools.chain((args[:3]), (headers,), args[4:]))\n     elif \"headers\" in kwargs and kwargs[\"headers\"]:\n", "issue": "AttributeError: 'Retry' object has no attribute 'copy'\nThe `elasticapm` Python client fails with `AttributeError: 'Retry' object has no attribute 'copy'` during HTTP retries.\r\n\r\nThe Elastic APM UI points towards the following lines being responsible for the crash https://github.com/elastic/apm-agent-python/blob/c9a1f7a74b4b8e39af06f9b383037a3ae49ca42b/elasticapm/instrumentation/packages/urllib3.py#L66-L68 \r\n\r\n**To Reproduce**\r\n\r\nConfigure a `requests` session with [urllib3 retries](https://urllib3.readthedocs.io/en/stable/reference/urllib3.util.html) as described below. Then point it at a flaky endpoint.\r\n\r\n``` python\r\nclass TimeoutHTTPAdapter(HTTPAdapter):\r\n    def __init__(self, retries: int, *args: Any, **kwargs: Any) -> None:\r\n        self.timeout = kwargs.pop(\"timeout\", 30)\r\n        self._retries = retries\r\n        super().__init__(*args, **kwargs)\r\n\r\n    def send( \r\n        self, request: requests.PreparedRequest, **kwargs: Any\r\n    ) -> requests.Response:\r\n        kwargs.setdefault(\"timeout\", self.timeout)\r\n        return super().send(request, **kwargs)\r\n\r\ndef session_with_retries(\r\n    retries: int = 3,\r\n    backoff_factor: float = 1,\r\n    timeout: int = 30,\r\n    status_forcelist: Collection[int] = (408, 429, 500, 502, 503, 504),\r\n) -> requests.Session:\r\n\r\n    retry_strategy = Retry(\r\n        total=retries,\r\n        backoff_factor=backoff_factor,\r\n        status_forcelist=status_forcelist,\r\n        allowed_methods=Retry.DEFAULT_ALLOWED_METHODS.union([\"POST\", \"PATCH\"]),\r\n    )\r\n    adapter = TimeoutHTTPAdapter(\r\n        retries=retries, max_retries=retry_strategy, timeout=timeout\r\n    )\r\n\r\n    s = requests.Session()\r\n    s.mount(\"http://\", adapter)\r\n    s.mount(\"https://\", adapter)\r\n    return s\r\n```\r\n\r\n**Environment**\r\n- OS: Linux\r\n- Python version: Python 3.11\r\n- Framework and version: `urllib3==2.0.6` and `requests==2.31.0`\r\n- APM Server version: 8.10.4\r\n- Agent version: `elastic-apm==6.19.0`\r\n\r\n**Background**\r\n* This appears to be related to the recent urllib3 fixes https://github.com/elastic/apm-agent-python/pull/1822 and https://github.com/elastic/apm-agent-python/issues/1816 \nAttributeError: 'Retry' object has no attribute 'copy'\nThe `elasticapm` Python client fails with `AttributeError: 'Retry' object has no attribute 'copy'` during HTTP retries.\r\n\r\nThe Elastic APM UI points towards the following lines being responsible for the crash https://github.com/elastic/apm-agent-python/blob/c9a1f7a74b4b8e39af06f9b383037a3ae49ca42b/elasticapm/instrumentation/packages/urllib3.py#L66-L68 \r\n\r\n**To Reproduce**\r\n\r\nConfigure a `requests` session with [urllib3 retries](https://urllib3.readthedocs.io/en/stable/reference/urllib3.util.html) as described below. Then point it at a flaky endpoint.\r\n\r\n``` python\r\nclass TimeoutHTTPAdapter(HTTPAdapter):\r\n    def __init__(self, retries: int, *args: Any, **kwargs: Any) -> None:\r\n        self.timeout = kwargs.pop(\"timeout\", 30)\r\n        self._retries = retries\r\n        super().__init__(*args, **kwargs)\r\n\r\n    def send( \r\n        self, request: requests.PreparedRequest, **kwargs: Any\r\n    ) -> requests.Response:\r\n        kwargs.setdefault(\"timeout\", self.timeout)\r\n        return super().send(request, **kwargs)\r\n\r\ndef session_with_retries(\r\n    retries: int = 3,\r\n    backoff_factor: float = 1,\r\n    timeout: int = 30,\r\n    status_forcelist: Collection[int] = (408, 429, 500, 502, 503, 504),\r\n) -> requests.Session:\r\n\r\n    retry_strategy = Retry(\r\n        total=retries,\r\n        backoff_factor=backoff_factor,\r\n        status_forcelist=status_forcelist,\r\n        allowed_methods=Retry.DEFAULT_ALLOWED_METHODS.union([\"POST\", \"PATCH\"]),\r\n    )\r\n    adapter = TimeoutHTTPAdapter(\r\n        retries=retries, max_retries=retry_strategy, timeout=timeout\r\n    )\r\n\r\n    s = requests.Session()\r\n    s.mount(\"http://\", adapter)\r\n    s.mount(\"https://\", adapter)\r\n    return s\r\n```\r\n\r\n**Environment**\r\n- OS: Linux\r\n- Python version: Python 3.11\r\n- Framework and version: `urllib3==2.0.6` and `requests==2.31.0`\r\n- APM Server version: 8.10.4\r\n- Agent version: `elastic-apm==6.19.0`\r\n\r\n**Background**\r\n* This appears to be related to the recent urllib3 fixes https://github.com/elastic/apm-agent-python/pull/1822 and https://github.com/elastic/apm-agent-python/issues/1816 \n", "before_files": [{"content": "#  BSD 3-Clause License\n#\n#  Copyright (c) 2019, 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\nimport itertools\n\nfrom elasticapm.conf import constants\nfrom elasticapm.instrumentation.packages.base import AbstractInstrumentedModule\nfrom elasticapm.traces import DroppedSpan, capture_span, execution_context\nfrom elasticapm.utils import default_ports\nfrom elasticapm.utils.disttracing import TracingOptions\n\n\ndef _set_disttracing_headers(headers, trace_parent, transaction) -> None:\n    trace_parent_str = trace_parent.to_string()\n    headers[constants.TRACEPARENT_HEADER_NAME] = trace_parent_str\n    if transaction.tracer.config.use_elastic_traceparent_header:\n        headers[constants.TRACEPARENT_LEGACY_HEADER_NAME] = trace_parent_str\n    if trace_parent.tracestate:\n        headers[constants.TRACESTATE_HEADER_NAME] = trace_parent.tracestate\n\n\ndef update_headers(args, kwargs, instance, transaction, trace_parent):\n    \"\"\"\n    The headers might be in 3 different places: as 4th positional argument, as \"headers\" keyword argument,\n    or, if none of the former two are provided, as instance variable on the HTTPConnection object.\n\n    If the headers are in the positional arguments tuple, a new tuple with updated headers will be returned.\n    If they are in the keyword arguments or on the instance, an updated kwargs dict will be returned\n\n    :param args: list of positional arguments\n    :param kwargs: dict of keyword arguments\n    :param instance: the HTTPConnection instance\n    :param transaction: the Transaction object\n    :param trace_parent: the TraceParent object\n    :return: an (args, kwargs) tuple\n    \"\"\"\n    from urllib3._version import __version__ as urllib3_version\n\n    if urllib3_version.startswith(\"2\") and len(args) >= 5 and args[4]:\n        headers = args[4].copy()\n        args = tuple(itertools.chain((args[:4]), (headers,), args[5:]))\n    elif len(args) >= 4 and args[3]:\n        headers = args[3].copy()\n        args = tuple(itertools.chain((args[:3]), (headers,), args[4:]))\n    elif \"headers\" in kwargs and kwargs[\"headers\"]:\n        headers = kwargs[\"headers\"].copy()\n        kwargs[\"headers\"] = headers\n    else:\n        headers = instance.headers.copy() if instance.headers else {}\n        # we don't want to change the instance headers, so we'll cheat and\n        # set the headers as keywords. This slightly changes how the wrapped\n        # method is called compared to uninstrumented code.\n        kwargs[\"headers\"] = headers\n    _set_disttracing_headers(headers, trace_parent, transaction)\n    return args, kwargs\n\n\nclass Urllib3Instrumentation(AbstractInstrumentedModule):\n    name = \"urllib3\"\n\n    instrument_list = [\n        (\"urllib3.connectionpool\", \"HTTPConnectionPool.urlopen\"),\n        # packages that vendor or vendored urllib3 in the past\n        (\"requests.packages.urllib3.connectionpool\", \"HTTPConnectionPool.urlopen\"),\n        (\"botocore.vendored.requests.packages.urllib3.connectionpool\", \"HTTPConnectionPool.urlopen\"),\n    ]\n\n    def call(self, module, method, wrapped, instance, args, kwargs):\n        if \"method\" in kwargs:\n            method = kwargs[\"method\"]\n        else:\n            method = args[0]\n\n        host = instance.host\n\n        if instance.port != default_ports.get(instance.scheme):\n            host += \":\" + str(instance.port)\n\n        if \"url\" in kwargs:\n            url = kwargs[\"url\"]\n        else:\n            url = args[1]\n\n        signature = method.upper() + \" \" + host\n\n        if url.startswith(\"/\"):\n            url = \"%s://%s%s\" % (instance.scheme, host, url)\n\n        transaction = execution_context.get_transaction()\n\n        with capture_span(\n            signature,\n            span_type=\"external\",\n            span_subtype=\"http\",\n            extra={\"http\": {\"url\": url}},\n            leaf=True,\n        ) as span:\n            # if urllib3 has been called in a leaf span, this span might be a DroppedSpan.\n            leaf_span = span\n            while isinstance(leaf_span, DroppedSpan):\n                leaf_span = leaf_span.parent\n\n            parent_id = leaf_span.id if leaf_span else transaction.id\n            trace_parent = transaction.trace_parent.copy_from(\n                span_id=parent_id, trace_options=TracingOptions(recorded=True)\n            )\n            args, kwargs = update_headers(args, kwargs, instance, transaction, trace_parent)\n            if leaf_span:\n                leaf_span.dist_tracing_propagated = True\n            response = wrapped(*args, **kwargs)\n            if response:\n                if span.context:\n                    span.context[\"http\"][\"status_code\"] = response.status\n                span.set_success() if response.status < 400 else span.set_failure()\n            return response\n\n    def mutate_unsampled_call_args(self, module, method, wrapped, instance, args, kwargs, transaction):\n        # since we don't have a span, we set the span id to the transaction id\n        trace_parent = transaction.trace_parent.copy_from(\n            span_id=transaction.id, trace_options=TracingOptions(recorded=False)\n        )\n        return update_headers(args, kwargs, instance, transaction, trace_parent)\n", "path": "elasticapm/instrumentation/packages/urllib3.py"}], "after_files": [{"content": "#  BSD 3-Clause License\n#\n#  Copyright (c) 2019, 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\nimport itertools\n\nfrom elasticapm.conf import constants\nfrom elasticapm.instrumentation.packages.base import AbstractInstrumentedModule\nfrom elasticapm.traces import DroppedSpan, capture_span, execution_context\nfrom elasticapm.utils import default_ports\nfrom elasticapm.utils.disttracing import TracingOptions\n\n\ndef _set_disttracing_headers(headers, trace_parent, transaction) -> None:\n    trace_parent_str = trace_parent.to_string()\n    headers[constants.TRACEPARENT_HEADER_NAME] = trace_parent_str\n    if transaction.tracer.config.use_elastic_traceparent_header:\n        headers[constants.TRACEPARENT_LEGACY_HEADER_NAME] = trace_parent_str\n    if trace_parent.tracestate:\n        headers[constants.TRACESTATE_HEADER_NAME] = trace_parent.tracestate\n\n\ndef update_headers(args, kwargs, instance, transaction, trace_parent):\n    \"\"\"\n    The headers might be in 3 different places: as 4th positional argument, as \"headers\" keyword argument,\n    or, if none of the former two are provided, as instance variable on the HTTPConnection object.\n\n    If the headers are in the positional arguments tuple, a new tuple with updated headers will be returned.\n    If they are in the keyword arguments or on the instance, an updated kwargs dict will be returned\n\n    :param args: list of positional arguments\n    :param kwargs: dict of keyword arguments\n    :param instance: the HTTPConnection instance\n    :param transaction: the Transaction object\n    :param trace_parent: the TraceParent object\n    :return: an (args, kwargs) tuple\n    \"\"\"\n    if len(args) >= 4 and args[3]:\n        headers = args[3].copy()\n        args = tuple(itertools.chain((args[:3]), (headers,), args[4:]))\n    elif \"headers\" in kwargs and kwargs[\"headers\"]:\n        headers = kwargs[\"headers\"].copy()\n        kwargs[\"headers\"] = headers\n    else:\n        headers = instance.headers.copy() if instance.headers else {}\n        # we don't want to change the instance headers, so we'll cheat and\n        # set the headers as keywords. This slightly changes how the wrapped\n        # method is called compared to uninstrumented code.\n        kwargs[\"headers\"] = headers\n    _set_disttracing_headers(headers, trace_parent, transaction)\n    return args, kwargs\n\n\nclass Urllib3Instrumentation(AbstractInstrumentedModule):\n    name = \"urllib3\"\n\n    instrument_list = [\n        (\"urllib3.connectionpool\", \"HTTPConnectionPool.urlopen\"),\n        # packages that vendor or vendored urllib3 in the past\n        (\"requests.packages.urllib3.connectionpool\", \"HTTPConnectionPool.urlopen\"),\n        (\"botocore.vendored.requests.packages.urllib3.connectionpool\", \"HTTPConnectionPool.urlopen\"),\n    ]\n\n    def call(self, module, method, wrapped, instance, args, kwargs):\n        if \"method\" in kwargs:\n            method = kwargs[\"method\"]\n        else:\n            method = args[0]\n\n        host = instance.host\n\n        if instance.port != default_ports.get(instance.scheme):\n            host += \":\" + str(instance.port)\n\n        if \"url\" in kwargs:\n            url = kwargs[\"url\"]\n        else:\n            url = args[1]\n\n        signature = method.upper() + \" \" + host\n\n        if url.startswith(\"/\"):\n            url = \"%s://%s%s\" % (instance.scheme, host, url)\n\n        transaction = execution_context.get_transaction()\n\n        with capture_span(\n            signature,\n            span_type=\"external\",\n            span_subtype=\"http\",\n            extra={\"http\": {\"url\": url}},\n            leaf=True,\n        ) as span:\n            # if urllib3 has been called in a leaf span, this span might be a DroppedSpan.\n            leaf_span = span\n            while isinstance(leaf_span, DroppedSpan):\n                leaf_span = leaf_span.parent\n\n            parent_id = leaf_span.id if leaf_span else transaction.id\n            trace_parent = transaction.trace_parent.copy_from(\n                span_id=parent_id, trace_options=TracingOptions(recorded=True)\n            )\n            args, kwargs = update_headers(args, kwargs, instance, transaction, trace_parent)\n            if leaf_span:\n                leaf_span.dist_tracing_propagated = True\n            response = wrapped(*args, **kwargs)\n            if response:\n                if span.context:\n                    span.context[\"http\"][\"status_code\"] = response.status\n                span.set_success() if response.status < 400 else span.set_failure()\n            return response\n\n    def mutate_unsampled_call_args(self, module, method, wrapped, instance, args, kwargs, transaction):\n        # since we don't have a span, we set the span id to the transaction id\n        trace_parent = transaction.trace_parent.copy_from(\n            span_id=transaction.id, trace_options=TracingOptions(recorded=False)\n        )\n        return update_headers(args, kwargs, instance, transaction, trace_parent)\n", "path": "elasticapm/instrumentation/packages/urllib3.py"}]}