We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
FastApi / Starlette setup without client argument not working
**Describe the bug**: 

The `ElasticAPM` middleware for Starlette appears to be missing a default value for the `client` parameter, resulting in a `TypeError` when using the middleware without explicitly providing a `client`. 

**To Reproduce**

1. Add the `ElasticAPM` middleware without explicitly providing a `client`.
2. Observe the `TypeError` mentioned above.

**Environment (please complete the following information)**
- OS: Linux
- Python version: 3.11
- Framework and version [e.g. Django 2.1]: fastapi 0.104.1
- APM Server version: ---
- Agent version: 6.20.0


**Additional context**

Add any other context about the problem here.

### Error:

The `ElasticAPM` middleware should allow for the `client` parameter to be optional, as indicated in the documentation, and should default to `None` if not provided.
From docs: https://www.elastic.co/guide/en/apm/agent/python/current/starlette-support.html

 ```
from starlette.applications import Starlette
from elasticapm.contrib.starlette import ElasticAPM

app = Starlette()
app.add_middleware(ElasticAPM)
```

Result:
 ```
.venv/lib/python3.11/site-packages/fastapi/applications.py", line 1015, in build_middleware_stack
    app = cls(app=app, **options)
          ^^^^^^^^^^^^^^^^^^^^^^^
TypeError: ElasticAPM.__init__() missing 1 required positional argument: 'client'
```

in the doc string shows that client its optional in type, but doesnt have None as default value, so passing:
 ```
app.add_middleware(ElasticAPM, client=None)
```

it seems to work.

### Proposed Solution
Change the `ElasticAPM` constructor signature to include `Optional[Client] = None` for the `client` parameter, making it optional with a default value of `None` in https://github.com/elastic/apm-agent-python/blob/4f5661277becc1034ee588bae4b018a4b22cc02b/elasticapm/contrib/starlette/__init__.py#L108C41-L108C41.

```python
def __init__(self, app: ASGIApp, client: Optional[Client] = None, **kwargs) -> None:
    """
    Args:
        app (ASGIApp): Starlette app
        client (Optional[Client]): ElasticAPM Client
    """
    ```




--- 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/starlette/__init__.py`
Content:
```
1 #  BSD 3-Clause License
2 #
3 #  Copyright (c) 2012, the Sentry Team, see AUTHORS for more details
4 #  Copyright (c) 2019, Elasticsearch BV
5 #  All rights reserved.
6 #
7 #  Redistribution and use in source and binary forms, with or without
8 #  modification, are permitted provided that the following conditions are met:
9 #
10 #  * Redistributions of source code must retain the above copyright notice, this
11 #    list of conditions and the following disclaimer.
12 #
13 #  * Redistributions in binary form must reproduce the above copyright notice,
14 #    this list of conditions and the following disclaimer in the documentation
15 #    and/or other materials provided with the distribution.
16 #
17 #  * Neither the name of the copyright holder nor the names of its
18 #    contributors may be used to endorse or promote products derived from
19 #    this software without specific prior written permission.
20 #
21 #  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
22 #  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
23 #  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
24 #  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
25 #  FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
26 #  DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
27 #  SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
28 #  CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
29 #  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30 
31 
32 from __future__ import absolute_import
33 
34 import asyncio
35 import functools
36 from typing import Dict, Optional
37 
38 import starlette
39 from starlette.requests import Request
40 from starlette.routing import Match, Mount
41 from starlette.types import ASGIApp, Message
42 
43 import elasticapm
44 import elasticapm.instrumentation.control
45 from elasticapm.base import Client, get_client
46 from elasticapm.conf import constants
47 from elasticapm.contrib.asyncio.traces import set_context
48 from elasticapm.contrib.starlette.utils import get_body, get_data_from_request, get_data_from_response
49 from elasticapm.utils.disttracing import TraceParent
50 from elasticapm.utils.encoding import long_field
51 from elasticapm.utils.logging import get_logger
52 
53 logger = get_logger("elasticapm.errors.client")
54 
55 
56 def make_apm_client(config: Optional[Dict] = None, client_cls=Client, **defaults) -> Client:
57     """Builds ElasticAPM client.
58 
59     Args:
60         config (dict): Dictionary of Client configuration. All keys must be uppercase. See `elasticapm.conf.Config`.
61         client_cls (Client): Must be Client or its child.
62         **defaults: Additional parameters for Client. See `elasticapm.base.Client`
63 
64     Returns:
65         Client
66     """
67     if "framework_name" not in defaults:
68         defaults["framework_name"] = "starlette"
69         defaults["framework_version"] = starlette.__version__
70 
71     return client_cls(config, **defaults)
72 
73 
74 class ElasticAPM:
75     """
76     Starlette / FastAPI middleware for Elastic APM capturing.
77 
78     >>> apm = make_apm_client({
79         >>> 'SERVICE_NAME': 'myapp',
80         >>> 'DEBUG': True,
81         >>> 'SERVER_URL': 'http://localhost:8200',
82         >>> 'CAPTURE_HEADERS': True,
83         >>> 'CAPTURE_BODY': 'all'
84     >>> })
85 
86     >>> app.add_middleware(ElasticAPM, client=apm)
87 
88     Pass an arbitrary SERVICE_NAME and SECRET_TOKEN::
89 
90     >>> elasticapm = ElasticAPM(app, service_name='myapp', secret_token='asdasdasd')
91 
92     Pass an explicit client (don't pass in additional options in this case)::
93 
94     >>> elasticapm = ElasticAPM(app, client=client)
95 
96     Capture an exception::
97 
98     >>> try:
99     >>>     1 / 0
100     >>> except ZeroDivisionError:
101     >>>     elasticapm.capture_exception()
102 
103     Capture a message::
104 
105     >>> elasticapm.capture_message('hello, world!')
106     """
107 
108     def __init__(self, app: ASGIApp, client: Optional[Client], **kwargs) -> None:
109         """
110 
111         Args:
112             app (ASGIApp): Starlette app
113             client (Client): ElasticAPM Client
114         """
115         if client:
116             self.client = client
117         else:
118             self.client = get_client()
119         if not self.client:
120             self.client = make_apm_client(**kwargs)
121 
122         if self.client.config.instrument and self.client.config.enabled:
123             elasticapm.instrumentation.control.instrument()
124 
125         # If we ever make this a general-use ASGI middleware we should use
126         # `asgiref.compatibility.guarantee_single_callable(app)` here
127         self.app = app
128 
129     async def __call__(self, scope, receive, send):
130         """
131         Args:
132             scope: ASGI scope dictionary
133             receive: receive awaitable callable
134             send: send awaitable callable
135         """
136         # we only handle the http scope, skip anything else.
137         if scope["type"] != "http" or (scope["type"] == "http" and self.client.should_ignore_url(scope["path"])):
138             await self.app(scope, receive, send)
139             return
140 
141         @functools.wraps(send)
142         async def wrapped_send(message) -> None:
143             if message.get("type") == "http.response.start":
144                 await set_context(
145                     lambda: get_data_from_response(message, self.client.config, constants.TRANSACTION), "response"
146                 )
147                 result = "HTTP {}xx".format(message["status"] // 100)
148                 elasticapm.set_transaction_result(result, override=False)
149             await send(message)
150 
151         _mocked_receive = None
152         _request_receive = None
153 
154         if self.client.config.capture_body != "off":
155 
156             # When we consume the body from receive, we replace the streaming
157             # mechanism with a mocked version -- this workaround came from
158             # https://github.com/encode/starlette/issues/495#issuecomment-513138055
159             body = []
160             while True:
161                 message = await receive()
162                 if not message:
163                     break
164                 if message["type"] == "http.request":
165                     b = message.get("body", b"")
166                     if b:
167                         body.append(b)
168                     if not message.get("more_body", False):
169                         break
170                 if message["type"] == "http.disconnect":
171                     break
172 
173             joined_body = b"".join(body)
174 
175             async def mocked_receive() -> Message:
176                 await asyncio.sleep(0)
177                 return {"type": "http.request", "body": long_field(joined_body)}
178 
179             _mocked_receive = mocked_receive
180 
181             async def request_receive() -> Message:
182                 await asyncio.sleep(0)
183                 return {"type": "http.request", "body": joined_body}
184 
185             _request_receive = request_receive
186 
187         request = Request(scope, receive=_mocked_receive or receive)
188         await self._request_started(request)
189 
190         # We don't end the transaction here, we rely on the starlette
191         # instrumentation of ServerErrorMiddleware to end the transaction
192         try:
193             await self.app(scope, _request_receive or receive, wrapped_send)
194             elasticapm.set_transaction_outcome(constants.OUTCOME.SUCCESS, override=False)
195         except Exception:
196             await self.capture_exception(
197                 context={"request": await get_data_from_request(request, self.client.config, constants.ERROR)}
198             )
199             elasticapm.set_transaction_result("HTTP 5xx", override=False)
200             elasticapm.set_transaction_outcome(constants.OUTCOME.FAILURE, override=False)
201             elasticapm.set_context({"status_code": 500}, "response")
202 
203             raise
204 
205     async def capture_exception(self, *args, **kwargs) -> None:
206         """Captures your exception.
207 
208         Args:
209             *args:
210             **kwargs:
211         """
212         self.client.capture_exception(*args, **kwargs)
213 
214     async def capture_message(self, *args, **kwargs) -> None:
215         """Captures your message.
216 
217         Args:
218             *args: Whatever
219             **kwargs: Whatever
220         """
221         self.client.capture_message(*args, **kwargs)
222 
223     async def _request_started(self, request: Request) -> None:
224         """Captures the begin of the request processing to APM.
225 
226         Args:
227             request (Request)
228         """
229         # When we consume the body, we replace the streaming mechanism with
230         # a mocked version -- this workaround came from
231         # https://github.com/encode/starlette/issues/495#issuecomment-513138055
232         # and we call the workaround here to make sure that regardless of
233         # `capture_body` settings, we will have access to the body if we need it.
234         if self.client.config.capture_body != "off":
235             await get_body(request)
236 
237         trace_parent = TraceParent.from_headers(dict(request.headers))
238         self.client.begin_transaction("request", trace_parent=trace_parent)
239 
240         await set_context(lambda: get_data_from_request(request, self.client.config, constants.TRANSACTION), "request")
241         transaction_name = self.get_route_name(request) or request.url.path
242         elasticapm.set_transaction_name("{} {}".format(request.method, transaction_name), override=False)
243 
244     def get_route_name(self, request: Request) -> str:
245         app = request.app
246         scope = request.scope
247         routes = app.routes
248         route_name = self._get_route_name(scope, routes)
249 
250         # Starlette magically redirects requests if the path matches a route name with a trailing slash
251         # appended or removed. To not spam the transaction names list, we do the same here and put these
252         # redirects all in the same "redirect trailing slashes" transaction name
253         if not route_name and app.router.redirect_slashes and scope["path"] != "/":
254             redirect_scope = dict(scope)
255             if scope["path"].endswith("/"):
256                 redirect_scope["path"] = scope["path"][:-1]
257                 trim = True
258             else:
259                 redirect_scope["path"] = scope["path"] + "/"
260                 trim = False
261 
262             route_name = self._get_route_name(redirect_scope, routes)
263             if route_name is not None:
264                 route_name = route_name + "/" if trim else route_name[:-1]
265         return route_name
266 
267     def _get_route_name(self, scope, routes, route_name=None):
268         for route in routes:
269             match, child_scope = route.matches(scope)
270             if match == Match.FULL:
271                 route_name = route.path
272                 child_scope = {**scope, **child_scope}
273                 if isinstance(route, Mount) and route.routes:
274                     child_route_name = self._get_route_name(child_scope, route.routes, route_name)
275                     if child_route_name is None:
276                         route_name = None
277                     else:
278                         route_name += child_route_name
279                 return route_name
280             elif match == Match.PARTIAL and route_name is None:
281                 route_name = route.path
282
```
--- 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/starlette/__init__.py b/elasticapm/contrib/starlette/__init__.py
--- a/elasticapm/contrib/starlette/__init__.py
+++ b/elasticapm/contrib/starlette/__init__.py
@@ -105,7 +105,7 @@
     >>> elasticapm.capture_message('hello, world!')
     """
 
-    def __init__(self, app: ASGIApp, client: Optional[Client], **kwargs) -> None:
+    def __init__(self, app: ASGIApp, client: Optional[Client] = None, **kwargs) -> None:
         """
 
         Args:

{"golden_diff": "diff --git a/elasticapm/contrib/starlette/__init__.py b/elasticapm/contrib/starlette/__init__.py\n--- a/elasticapm/contrib/starlette/__init__.py\n+++ b/elasticapm/contrib/starlette/__init__.py\n@@ -105,7 +105,7 @@\n     >>> elasticapm.capture_message('hello, world!')\n     \"\"\"\n \n-    def __init__(self, app: ASGIApp, client: Optional[Client], **kwargs) -> None:\n+    def __init__(self, app: ASGIApp, client: Optional[Client] = None, **kwargs) -> None:\n         \"\"\"\n \n         Args:\n", "issue": "FastApi / Starlette setup without client argument not working\n**Describe the bug**: \r\n\r\nThe `ElasticAPM` middleware for Starlette appears to be missing a default value for the `client` parameter, resulting in a `TypeError` when using the middleware without explicitly providing a `client`. \r\n\r\n**To Reproduce**\r\n\r\n1. Add the `ElasticAPM` middleware without explicitly providing a `client`.\r\n2. Observe the `TypeError` mentioned above.\r\n\r\n**Environment (please complete the following information)**\r\n- OS: Linux\r\n- Python version: 3.11\r\n- Framework and version [e.g. Django 2.1]: fastapi 0.104.1\r\n- APM Server version: ---\r\n- Agent version: 6.20.0\r\n\r\n\r\n**Additional context**\r\n\r\nAdd any other context about the problem here.\r\n\r\n### Error:\r\n\r\nThe `ElasticAPM` middleware should allow for the `client` parameter to be optional, as indicated in the documentation, and should default to `None` if not provided.\r\nFrom docs: https://www.elastic.co/guide/en/apm/agent/python/current/starlette-support.html\r\n\r\n ```\r\nfrom starlette.applications import Starlette\r\nfrom elasticapm.contrib.starlette import ElasticAPM\r\n\r\napp = Starlette()\r\napp.add_middleware(ElasticAPM)\r\n```\r\n\r\nResult:\r\n ```\r\n.venv/lib/python3.11/site-packages/fastapi/applications.py\", line 1015, in build_middleware_stack\r\n    app = cls(app=app, **options)\r\n          ^^^^^^^^^^^^^^^^^^^^^^^\r\nTypeError: ElasticAPM.__init__() missing 1 required positional argument: 'client'\r\n```\r\n\r\nin the doc string shows that client its optional in type, but doesnt have None as default value, so passing:\r\n ```\r\napp.add_middleware(ElasticAPM, client=None)\r\n```\r\n\r\nit seems to work.\r\n\r\n### Proposed Solution\r\nChange the `ElasticAPM` constructor signature to include `Optional[Client] = None` for the `client` parameter, making it optional with a default value of `None` in https://github.com/elastic/apm-agent-python/blob/4f5661277becc1034ee588bae4b018a4b22cc02b/elasticapm/contrib/starlette/__init__.py#L108C41-L108C41.\r\n\r\n```python\r\ndef __init__(self, app: ASGIApp, client: Optional[Client] = None, **kwargs) -> None:\r\n    \"\"\"\r\n    Args:\r\n        app (ASGIApp): Starlette app\r\n        client (Optional[Client]): ElasticAPM Client\r\n    \"\"\"\r\n    ```\r\n\r\n\r\n\n", "before_files": [{"content": "#  BSD 3-Clause License\n#\n#  Copyright (c) 2012, the Sentry Team, see AUTHORS for more details\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\n\nfrom __future__ import absolute_import\n\nimport asyncio\nimport functools\nfrom typing import Dict, Optional\n\nimport starlette\nfrom starlette.requests import Request\nfrom starlette.routing import Match, Mount\nfrom starlette.types import ASGIApp, Message\n\nimport elasticapm\nimport elasticapm.instrumentation.control\nfrom elasticapm.base import Client, get_client\nfrom elasticapm.conf import constants\nfrom elasticapm.contrib.asyncio.traces import set_context\nfrom elasticapm.contrib.starlette.utils import get_body, get_data_from_request, get_data_from_response\nfrom elasticapm.utils.disttracing import TraceParent\nfrom elasticapm.utils.encoding import long_field\nfrom elasticapm.utils.logging import get_logger\n\nlogger = get_logger(\"elasticapm.errors.client\")\n\n\ndef make_apm_client(config: Optional[Dict] = None, client_cls=Client, **defaults) -> Client:\n    \"\"\"Builds ElasticAPM client.\n\n    Args:\n        config (dict): Dictionary of Client configuration. All keys must be uppercase. See `elasticapm.conf.Config`.\n        client_cls (Client): Must be Client or its child.\n        **defaults: Additional parameters for Client. See `elasticapm.base.Client`\n\n    Returns:\n        Client\n    \"\"\"\n    if \"framework_name\" not in defaults:\n        defaults[\"framework_name\"] = \"starlette\"\n        defaults[\"framework_version\"] = starlette.__version__\n\n    return client_cls(config, **defaults)\n\n\nclass ElasticAPM:\n    \"\"\"\n    Starlette / FastAPI middleware for Elastic APM capturing.\n\n    >>> apm = make_apm_client({\n        >>> 'SERVICE_NAME': 'myapp',\n        >>> 'DEBUG': True,\n        >>> 'SERVER_URL': 'http://localhost:8200',\n        >>> 'CAPTURE_HEADERS': True,\n        >>> 'CAPTURE_BODY': 'all'\n    >>> })\n\n    >>> app.add_middleware(ElasticAPM, client=apm)\n\n    Pass an arbitrary SERVICE_NAME and SECRET_TOKEN::\n\n    >>> elasticapm = ElasticAPM(app, service_name='myapp', secret_token='asdasdasd')\n\n    Pass an explicit client (don't pass in additional options in this case)::\n\n    >>> elasticapm = ElasticAPM(app, client=client)\n\n    Capture an exception::\n\n    >>> try:\n    >>>     1 / 0\n    >>> except ZeroDivisionError:\n    >>>     elasticapm.capture_exception()\n\n    Capture a message::\n\n    >>> elasticapm.capture_message('hello, world!')\n    \"\"\"\n\n    def __init__(self, app: ASGIApp, client: Optional[Client], **kwargs) -> None:\n        \"\"\"\n\n        Args:\n            app (ASGIApp): Starlette app\n            client (Client): ElasticAPM Client\n        \"\"\"\n        if client:\n            self.client = client\n        else:\n            self.client = get_client()\n        if not self.client:\n            self.client = make_apm_client(**kwargs)\n\n        if self.client.config.instrument and self.client.config.enabled:\n            elasticapm.instrumentation.control.instrument()\n\n        # If we ever make this a general-use ASGI middleware we should use\n        # `asgiref.compatibility.guarantee_single_callable(app)` here\n        self.app = app\n\n    async def __call__(self, scope, receive, send):\n        \"\"\"\n        Args:\n            scope: ASGI scope dictionary\n            receive: receive awaitable callable\n            send: send awaitable callable\n        \"\"\"\n        # we only handle the http scope, skip anything else.\n        if scope[\"type\"] != \"http\" or (scope[\"type\"] == \"http\" and self.client.should_ignore_url(scope[\"path\"])):\n            await self.app(scope, receive, send)\n            return\n\n        @functools.wraps(send)\n        async def wrapped_send(message) -> None:\n            if message.get(\"type\") == \"http.response.start\":\n                await set_context(\n                    lambda: get_data_from_response(message, self.client.config, constants.TRANSACTION), \"response\"\n                )\n                result = \"HTTP {}xx\".format(message[\"status\"] // 100)\n                elasticapm.set_transaction_result(result, override=False)\n            await send(message)\n\n        _mocked_receive = None\n        _request_receive = None\n\n        if self.client.config.capture_body != \"off\":\n\n            # When we consume the body from receive, we replace the streaming\n            # mechanism with a mocked version -- this workaround came from\n            # https://github.com/encode/starlette/issues/495#issuecomment-513138055\n            body = []\n            while True:\n                message = await receive()\n                if not message:\n                    break\n                if message[\"type\"] == \"http.request\":\n                    b = message.get(\"body\", b\"\")\n                    if b:\n                        body.append(b)\n                    if not message.get(\"more_body\", False):\n                        break\n                if message[\"type\"] == \"http.disconnect\":\n                    break\n\n            joined_body = b\"\".join(body)\n\n            async def mocked_receive() -> Message:\n                await asyncio.sleep(0)\n                return {\"type\": \"http.request\", \"body\": long_field(joined_body)}\n\n            _mocked_receive = mocked_receive\n\n            async def request_receive() -> Message:\n                await asyncio.sleep(0)\n                return {\"type\": \"http.request\", \"body\": joined_body}\n\n            _request_receive = request_receive\n\n        request = Request(scope, receive=_mocked_receive or receive)\n        await self._request_started(request)\n\n        # We don't end the transaction here, we rely on the starlette\n        # instrumentation of ServerErrorMiddleware to end the transaction\n        try:\n            await self.app(scope, _request_receive or receive, wrapped_send)\n            elasticapm.set_transaction_outcome(constants.OUTCOME.SUCCESS, override=False)\n        except Exception:\n            await self.capture_exception(\n                context={\"request\": await get_data_from_request(request, self.client.config, constants.ERROR)}\n            )\n            elasticapm.set_transaction_result(\"HTTP 5xx\", override=False)\n            elasticapm.set_transaction_outcome(constants.OUTCOME.FAILURE, override=False)\n            elasticapm.set_context({\"status_code\": 500}, \"response\")\n\n            raise\n\n    async def capture_exception(self, *args, **kwargs) -> None:\n        \"\"\"Captures your exception.\n\n        Args:\n            *args:\n            **kwargs:\n        \"\"\"\n        self.client.capture_exception(*args, **kwargs)\n\n    async def capture_message(self, *args, **kwargs) -> None:\n        \"\"\"Captures your message.\n\n        Args:\n            *args: Whatever\n            **kwargs: Whatever\n        \"\"\"\n        self.client.capture_message(*args, **kwargs)\n\n    async def _request_started(self, request: Request) -> None:\n        \"\"\"Captures the begin of the request processing to APM.\n\n        Args:\n            request (Request)\n        \"\"\"\n        # When we consume the body, we replace the streaming mechanism with\n        # a mocked version -- this workaround came from\n        # https://github.com/encode/starlette/issues/495#issuecomment-513138055\n        # and we call the workaround here to make sure that regardless of\n        # `capture_body` settings, we will have access to the body if we need it.\n        if self.client.config.capture_body != \"off\":\n            await get_body(request)\n\n        trace_parent = TraceParent.from_headers(dict(request.headers))\n        self.client.begin_transaction(\"request\", trace_parent=trace_parent)\n\n        await set_context(lambda: get_data_from_request(request, self.client.config, constants.TRANSACTION), \"request\")\n        transaction_name = self.get_route_name(request) or request.url.path\n        elasticapm.set_transaction_name(\"{} {}\".format(request.method, transaction_name), override=False)\n\n    def get_route_name(self, request: Request) -> str:\n        app = request.app\n        scope = request.scope\n        routes = app.routes\n        route_name = self._get_route_name(scope, routes)\n\n        # Starlette magically redirects requests if the path matches a route name with a trailing slash\n        # appended or removed. To not spam the transaction names list, we do the same here and put these\n        # redirects all in the same \"redirect trailing slashes\" transaction name\n        if not route_name and app.router.redirect_slashes and scope[\"path\"] != \"/\":\n            redirect_scope = dict(scope)\n            if scope[\"path\"].endswith(\"/\"):\n                redirect_scope[\"path\"] = scope[\"path\"][:-1]\n                trim = True\n            else:\n                redirect_scope[\"path\"] = scope[\"path\"] + \"/\"\n                trim = False\n\n            route_name = self._get_route_name(redirect_scope, routes)\n            if route_name is not None:\n                route_name = route_name + \"/\" if trim else route_name[:-1]\n        return route_name\n\n    def _get_route_name(self, scope, routes, route_name=None):\n        for route in routes:\n            match, child_scope = route.matches(scope)\n            if match == Match.FULL:\n                route_name = route.path\n                child_scope = {**scope, **child_scope}\n                if isinstance(route, Mount) and route.routes:\n                    child_route_name = self._get_route_name(child_scope, route.routes, route_name)\n                    if child_route_name is None:\n                        route_name = None\n                    else:\n                        route_name += child_route_name\n                return route_name\n            elif match == Match.PARTIAL and route_name is None:\n                route_name = route.path\n", "path": "elasticapm/contrib/starlette/__init__.py"}], "after_files": [{"content": "#  BSD 3-Clause License\n#\n#  Copyright (c) 2012, the Sentry Team, see AUTHORS for more details\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\n\nfrom __future__ import absolute_import\n\nimport asyncio\nimport functools\nfrom typing import Dict, Optional\n\nimport starlette\nfrom starlette.requests import Request\nfrom starlette.routing import Match, Mount\nfrom starlette.types import ASGIApp, Message\n\nimport elasticapm\nimport elasticapm.instrumentation.control\nfrom elasticapm.base import Client, get_client\nfrom elasticapm.conf import constants\nfrom elasticapm.contrib.asyncio.traces import set_context\nfrom elasticapm.contrib.starlette.utils import get_body, get_data_from_request, get_data_from_response\nfrom elasticapm.utils.disttracing import TraceParent\nfrom elasticapm.utils.encoding import long_field\nfrom elasticapm.utils.logging import get_logger\n\nlogger = get_logger(\"elasticapm.errors.client\")\n\n\ndef make_apm_client(config: Optional[Dict] = None, client_cls=Client, **defaults) -> Client:\n    \"\"\"Builds ElasticAPM client.\n\n    Args:\n        config (dict): Dictionary of Client configuration. All keys must be uppercase. See `elasticapm.conf.Config`.\n        client_cls (Client): Must be Client or its child.\n        **defaults: Additional parameters for Client. See `elasticapm.base.Client`\n\n    Returns:\n        Client\n    \"\"\"\n    if \"framework_name\" not in defaults:\n        defaults[\"framework_name\"] = \"starlette\"\n        defaults[\"framework_version\"] = starlette.__version__\n\n    return client_cls(config, **defaults)\n\n\nclass ElasticAPM:\n    \"\"\"\n    Starlette / FastAPI middleware for Elastic APM capturing.\n\n    >>> apm = make_apm_client({\n        >>> 'SERVICE_NAME': 'myapp',\n        >>> 'DEBUG': True,\n        >>> 'SERVER_URL': 'http://localhost:8200',\n        >>> 'CAPTURE_HEADERS': True,\n        >>> 'CAPTURE_BODY': 'all'\n    >>> })\n\n    >>> app.add_middleware(ElasticAPM, client=apm)\n\n    Pass an arbitrary SERVICE_NAME and SECRET_TOKEN::\n\n    >>> elasticapm = ElasticAPM(app, service_name='myapp', secret_token='asdasdasd')\n\n    Pass an explicit client (don't pass in additional options in this case)::\n\n    >>> elasticapm = ElasticAPM(app, client=client)\n\n    Capture an exception::\n\n    >>> try:\n    >>>     1 / 0\n    >>> except ZeroDivisionError:\n    >>>     elasticapm.capture_exception()\n\n    Capture a message::\n\n    >>> elasticapm.capture_message('hello, world!')\n    \"\"\"\n\n    def __init__(self, app: ASGIApp, client: Optional[Client] = None, **kwargs) -> None:\n        \"\"\"\n\n        Args:\n            app (ASGIApp): Starlette app\n            client (Client): ElasticAPM Client\n        \"\"\"\n        if client:\n            self.client = client\n        else:\n            self.client = get_client()\n        if not self.client:\n            self.client = make_apm_client(**kwargs)\n\n        if self.client.config.instrument and self.client.config.enabled:\n            elasticapm.instrumentation.control.instrument()\n\n        # If we ever make this a general-use ASGI middleware we should use\n        # `asgiref.compatibility.guarantee_single_callable(app)` here\n        self.app = app\n\n    async def __call__(self, scope, receive, send):\n        \"\"\"\n        Args:\n            scope: ASGI scope dictionary\n            receive: receive awaitable callable\n            send: send awaitable callable\n        \"\"\"\n        # we only handle the http scope, skip anything else.\n        if scope[\"type\"] != \"http\" or (scope[\"type\"] == \"http\" and self.client.should_ignore_url(scope[\"path\"])):\n            await self.app(scope, receive, send)\n            return\n\n        @functools.wraps(send)\n        async def wrapped_send(message) -> None:\n            if message.get(\"type\") == \"http.response.start\":\n                await set_context(\n                    lambda: get_data_from_response(message, self.client.config, constants.TRANSACTION), \"response\"\n                )\n                result = \"HTTP {}xx\".format(message[\"status\"] // 100)\n                elasticapm.set_transaction_result(result, override=False)\n            await send(message)\n\n        _mocked_receive = None\n        _request_receive = None\n\n        if self.client.config.capture_body != \"off\":\n\n            # When we consume the body from receive, we replace the streaming\n            # mechanism with a mocked version -- this workaround came from\n            # https://github.com/encode/starlette/issues/495#issuecomment-513138055\n            body = []\n            while True:\n                message = await receive()\n                if not message:\n                    break\n                if message[\"type\"] == \"http.request\":\n                    b = message.get(\"body\", b\"\")\n                    if b:\n                        body.append(b)\n                    if not message.get(\"more_body\", False):\n                        break\n                if message[\"type\"] == \"http.disconnect\":\n                    break\n\n            joined_body = b\"\".join(body)\n\n            async def mocked_receive() -> Message:\n                await asyncio.sleep(0)\n                return {\"type\": \"http.request\", \"body\": long_field(joined_body)}\n\n            _mocked_receive = mocked_receive\n\n            async def request_receive() -> Message:\n                await asyncio.sleep(0)\n                return {\"type\": \"http.request\", \"body\": joined_body}\n\n            _request_receive = request_receive\n\n        request = Request(scope, receive=_mocked_receive or receive)\n        await self._request_started(request)\n\n        # We don't end the transaction here, we rely on the starlette\n        # instrumentation of ServerErrorMiddleware to end the transaction\n        try:\n            await self.app(scope, _request_receive or receive, wrapped_send)\n            elasticapm.set_transaction_outcome(constants.OUTCOME.SUCCESS, override=False)\n        except Exception:\n            await self.capture_exception(\n                context={\"request\": await get_data_from_request(request, self.client.config, constants.ERROR)}\n            )\n            elasticapm.set_transaction_result(\"HTTP 5xx\", override=False)\n            elasticapm.set_transaction_outcome(constants.OUTCOME.FAILURE, override=False)\n            elasticapm.set_context({\"status_code\": 500}, \"response\")\n\n            raise\n\n    async def capture_exception(self, *args, **kwargs) -> None:\n        \"\"\"Captures your exception.\n\n        Args:\n            *args:\n            **kwargs:\n        \"\"\"\n        self.client.capture_exception(*args, **kwargs)\n\n    async def capture_message(self, *args, **kwargs) -> None:\n        \"\"\"Captures your message.\n\n        Args:\n            *args: Whatever\n            **kwargs: Whatever\n        \"\"\"\n        self.client.capture_message(*args, **kwargs)\n\n    async def _request_started(self, request: Request) -> None:\n        \"\"\"Captures the begin of the request processing to APM.\n\n        Args:\n            request (Request)\n        \"\"\"\n        # When we consume the body, we replace the streaming mechanism with\n        # a mocked version -- this workaround came from\n        # https://github.com/encode/starlette/issues/495#issuecomment-513138055\n        # and we call the workaround here to make sure that regardless of\n        # `capture_body` settings, we will have access to the body if we need it.\n        if self.client.config.capture_body != \"off\":\n            await get_body(request)\n\n        trace_parent = TraceParent.from_headers(dict(request.headers))\n        self.client.begin_transaction(\"request\", trace_parent=trace_parent)\n\n        await set_context(lambda: get_data_from_request(request, self.client.config, constants.TRANSACTION), \"request\")\n        transaction_name = self.get_route_name(request) or request.url.path\n        elasticapm.set_transaction_name(\"{} {}\".format(request.method, transaction_name), override=False)\n\n    def get_route_name(self, request: Request) -> str:\n        app = request.app\n        scope = request.scope\n        routes = app.routes\n        route_name = self._get_route_name(scope, routes)\n\n        # Starlette magically redirects requests if the path matches a route name with a trailing slash\n        # appended or removed. To not spam the transaction names list, we do the same here and put these\n        # redirects all in the same \"redirect trailing slashes\" transaction name\n        if not route_name and app.router.redirect_slashes and scope[\"path\"] != \"/\":\n            redirect_scope = dict(scope)\n            if scope[\"path\"].endswith(\"/\"):\n                redirect_scope[\"path\"] = scope[\"path\"][:-1]\n                trim = True\n            else:\n                redirect_scope[\"path\"] = scope[\"path\"] + \"/\"\n                trim = False\n\n            route_name = self._get_route_name(redirect_scope, routes)\n            if route_name is not None:\n                route_name = route_name + \"/\" if trim else route_name[:-1]\n        return route_name\n\n    def _get_route_name(self, scope, routes, route_name=None):\n        for route in routes:\n            match, child_scope = route.matches(scope)\n            if match == Match.FULL:\n                route_name = route.path\n                child_scope = {**scope, **child_scope}\n                if isinstance(route, Mount) and route.routes:\n                    child_route_name = self._get_route_name(child_scope, route.routes, route_name)\n                    if child_route_name is None:\n                        route_name = None\n                    else:\n                        route_name += child_route_name\n                return route_name\n            elif match == Match.PARTIAL and route_name is None:\n                route_name = route.path\n", "path": "elasticapm/contrib/starlette/__init__.py"}]}