We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
[Starlette] ordering of adding the ElasticAPM is no relevant and must be last middleware to be added
**Describe the bug**:

Since elastic-apm>=6.13.0 we did not get transaction information anymore from our FastAPI application. It turned out that since then the order of adding multiple layers of middleware is relevant. If a middleware is added after `elasticapm.contrib.starlette.ElasticAPM` the transaction information is missing when the transaction is closed here https://github.com/elastic/apm-agent-python/blob/main/elasticapm/instrumentation/packages/asyncio/starlette.py#L52. Meaning the call here https://github.com/elastic/apm-agent-python/blob/main/elasticapm/base.py#L324 returns None in such a case.

Reordering of adding the middleware layers does resolve the issue.

**To Reproduce**

1. create a FastAPI app:
 ```
import time

from fastapi import FastAPI
from elasticapm.contrib.starlette import make_apm_client, ElasticAPM
from starlette.middleware import Middleware
from starlette.middleware.base import BaseHTTPMiddleware


class CustomHeaderMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request, call_next):
        start_time = time.time()
        response = await call_next(request)
        process_time = time.time() - start_time
        response.headers["X-Process-Time"] = str(process_time)
        return response

apm = make_apm_client()
app = FastAPI()

# this ordering works
# app.add_middleware(CustomHeaderMiddleware)
app.add_middleware(ElasticAPM, client=apm)

# this order does not work
app.add_middleware(CustomHeaderMiddleware)

@app.get("/ok")
async def ok():
    return {"details": "ok"}


@app.get("/raise")
async def raise_route():
    raise ValueError("oho")

 ```

2. install requirements: elastic-apm, fastapi, uvicorn
3. run the app with an APM server running and define how to reach it:
```
ELASTIC_APM_SERVER_URL=http://localhost:8200
ELASTIC_APM_SECRET_TOKEN=
ELASTIC_APM_SERVICE_NAME=my-service
ELASTIC_APM_ENVIRONMENT=test
```

run it with: `uvicorn app:app`
4. check if transactions are send and complete in APM, which is not the case


**Environment (please complete the following information)**
- OS: Debian bullseye and OS X
- Python version: 3.10
- Framework and version: elastic-apm-6.13.2 fastapi-0.88.0 starlette-0.22.0 uvicorn-0.20.0
- APM Server version: 7.15.2 (local) and  8.4.2 on elastic-cloud.com
- Agent version: 6.13.0, 6.13.1, 6.13.2


**Additional context**

It seems that this happens since the change to the StarletteServerErrorMiddlewareInstrumentation done here https://github.com/elastic/apm-agent-python/pull/1674/files

- `requirements.txt`:
  <details>
    <summary>Click to expand</summary>

    ```
  elastic-apm==6.13.2
  fastapi==0.88.0
  uvicorn==0.20.0
    ```
  </details>


--- 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
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):
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 = make_apm_client(**kwargs)
119 
120         if self.client.config.instrument and self.client.config.enabled:
121             elasticapm.instrumentation.control.instrument()
122 
123         # If we ever make this a general-use ASGI middleware we should use
124         # `asgiref.compatibility.guarantee_single_callable(app)` here
125         self.app = app
126 
127     async def __call__(self, scope, receive, send):
128         """
129         Args:
130             scope: ASGI scope dictionary
131             receive: receive awaitable callable
132             send: send awaitable callable
133         """
134         # we only handle the http scope, skip anything else.
135         if scope["type"] != "http" or (scope["type"] == "http" and self.client.should_ignore_url(scope["path"])):
136             await self.app(scope, receive, send)
137             return
138 
139         @functools.wraps(send)
140         async def wrapped_send(message):
141             if message.get("type") == "http.response.start":
142                 await set_context(
143                     lambda: get_data_from_response(message, self.client.config, constants.TRANSACTION), "response"
144                 )
145                 result = "HTTP {}xx".format(message["status"] // 100)
146                 elasticapm.set_transaction_result(result, override=False)
147             await send(message)
148 
149         _mocked_receive = None
150         _request_receive = None
151 
152         if self.client.config.capture_body != "off":
153 
154             # When we consume the body from receive, we replace the streaming
155             # mechanism with a mocked version -- this workaround came from
156             # https://github.com/encode/starlette/issues/495#issuecomment-513138055
157             body = []
158             while True:
159                 message = await receive()
160                 if not message:
161                     break
162                 if message["type"] == "http.request":
163                     b = message.get("body", b"")
164                     if b:
165                         body.append(b)
166                     if not message.get("more_body", False):
167                         break
168                 if message["type"] == "http.disconnect":
169                     break
170 
171             joined_body = b"".join(body)
172 
173             async def mocked_receive() -> Message:
174                 await asyncio.sleep(0)
175                 return {"type": "http.request", "body": long_field(joined_body)}
176 
177             _mocked_receive = mocked_receive
178 
179             async def request_receive() -> Message:
180                 await asyncio.sleep(0)
181                 return {"type": "http.request", "body": joined_body}
182 
183             _request_receive = request_receive
184 
185         request = Request(scope, receive=_mocked_receive or receive)
186         await self._request_started(request)
187 
188         # We don't end the transaction here, we rely on the starlette
189         # instrumentation of ServerErrorMiddleware to end the transaction
190         try:
191             await self.app(scope, _request_receive or receive, wrapped_send)
192             elasticapm.set_transaction_outcome(constants.OUTCOME.SUCCESS, override=False)
193         except Exception:
194             await self.capture_exception(
195                 context={"request": await get_data_from_request(request, self.client.config, constants.ERROR)}
196             )
197             elasticapm.set_transaction_result("HTTP 5xx", override=False)
198             elasticapm.set_transaction_outcome(constants.OUTCOME.FAILURE, override=False)
199             elasticapm.set_context({"status_code": 500}, "response")
200 
201             raise
202 
203     async def capture_exception(self, *args, **kwargs):
204         """Captures your exception.
205 
206         Args:
207             *args:
208             **kwargs:
209         """
210         self.client.capture_exception(*args, **kwargs)
211 
212     async def capture_message(self, *args, **kwargs):
213         """Captures your message.
214 
215         Args:
216             *args: Whatever
217             **kwargs: Whatever
218         """
219         self.client.capture_message(*args, **kwargs)
220 
221     async def _request_started(self, request: Request):
222         """Captures the begin of the request processing to APM.
223 
224         Args:
225             request (Request)
226         """
227         # When we consume the body, we replace the streaming mechanism with
228         # a mocked version -- this workaround came from
229         # https://github.com/encode/starlette/issues/495#issuecomment-513138055
230         # and we call the workaround here to make sure that regardless of
231         # `capture_body` settings, we will have access to the body if we need it.
232         if self.client.config.capture_body != "off":
233             await get_body(request)
234 
235         trace_parent = TraceParent.from_headers(dict(request.headers))
236         self.client.begin_transaction("request", trace_parent=trace_parent)
237 
238         await set_context(lambda: get_data_from_request(request, self.client.config, constants.TRANSACTION), "request")
239         transaction_name = self.get_route_name(request) or request.url.path
240         elasticapm.set_transaction_name("{} {}".format(request.method, transaction_name), override=False)
241 
242     def get_route_name(self, request: Request) -> str:
243         app = request.app
244         scope = request.scope
245         routes = app.routes
246         route_name = self._get_route_name(scope, routes)
247 
248         # Starlette magically redirects requests if the path matches a route name with a trailing slash
249         # appended or removed. To not spam the transaction names list, we do the same here and put these
250         # redirects all in the same "redirect trailing slashes" transaction name
251         if not route_name and app.router.redirect_slashes and scope["path"] != "/":
252             redirect_scope = dict(scope)
253             if scope["path"].endswith("/"):
254                 redirect_scope["path"] = scope["path"][:-1]
255                 trim = True
256             else:
257                 redirect_scope["path"] = scope["path"] + "/"
258                 trim = False
259 
260             route_name = self._get_route_name(redirect_scope, routes)
261             if route_name is not None:
262                 route_name = route_name + "/" if trim else route_name[:-1]
263         return route_name
264 
265     def _get_route_name(self, scope, routes, route_name=None):
266         for route in routes:
267             match, child_scope = route.matches(scope)
268             if match == Match.FULL:
269                 route_name = route.path
270                 child_scope = {**scope, **child_scope}
271                 if isinstance(route, Mount) and route.routes:
272                     child_route_name = self._get_route_name(child_scope, route.routes, route_name)
273                     if child_route_name is None:
274                         route_name = None
275                     else:
276                         route_name += child_route_name
277                 return route_name
278             elif match == Match.PARTIAL and route_name is None:
279                 route_name = route.path
280
```
--- 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
@@ -42,7 +42,7 @@
 
 import elasticapm
 import elasticapm.instrumentation.control
-from elasticapm.base import Client
+from elasticapm.base import Client, get_client
 from elasticapm.conf import constants
 from elasticapm.contrib.asyncio.traces import set_context
 from elasticapm.contrib.starlette.utils import get_body, get_data_from_request, get_data_from_response
@@ -115,6 +115,8 @@
         if client:
             self.client = client
         else:
+            self.client = get_client()
+        if not self.client:
             self.client = make_apm_client(**kwargs)
 
         if self.client.config.instrument and self.client.config.enabled:

{"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@@ -42,7 +42,7 @@\n \n import elasticapm\n import elasticapm.instrumentation.control\n-from elasticapm.base import Client\n+from elasticapm.base import Client, get_client\n from elasticapm.conf import constants\n from elasticapm.contrib.asyncio.traces import set_context\n from elasticapm.contrib.starlette.utils import get_body, get_data_from_request, get_data_from_response\n@@ -115,6 +115,8 @@\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", "issue": "[Starlette] ordering of adding the ElasticAPM is no relevant and must be last middleware to be added\n**Describe the bug**:\r\n\r\nSince elastic-apm>=6.13.0 we did not get transaction information anymore from our FastAPI application. It turned out that since then the order of adding multiple layers of middleware is relevant. If a middleware is added after `elasticapm.contrib.starlette.ElasticAPM` the transaction information is missing when the transaction is closed here https://github.com/elastic/apm-agent-python/blob/main/elasticapm/instrumentation/packages/asyncio/starlette.py#L52. Meaning the call here https://github.com/elastic/apm-agent-python/blob/main/elasticapm/base.py#L324 returns None in such a case.\r\n\r\nReordering of adding the middleware layers does resolve the issue.\r\n\r\n**To Reproduce**\r\n\r\n1. create a FastAPI app:\r\n ```\r\nimport time\r\n\r\nfrom fastapi import FastAPI\r\nfrom elasticapm.contrib.starlette import make_apm_client, ElasticAPM\r\nfrom starlette.middleware import Middleware\r\nfrom starlette.middleware.base import BaseHTTPMiddleware\r\n\r\n\r\nclass CustomHeaderMiddleware(BaseHTTPMiddleware):\r\n    async def dispatch(self, request, call_next):\r\n        start_time = time.time()\r\n        response = await call_next(request)\r\n        process_time = time.time() - start_time\r\n        response.headers[\"X-Process-Time\"] = str(process_time)\r\n        return response\r\n\r\napm = make_apm_client()\r\napp = FastAPI()\r\n\r\n# this ordering works\r\n# app.add_middleware(CustomHeaderMiddleware)\r\napp.add_middleware(ElasticAPM, client=apm)\r\n\r\n# this order does not work\r\napp.add_middleware(CustomHeaderMiddleware)\r\n\r\n@app.get(\"/ok\")\r\nasync def ok():\r\n    return {\"details\": \"ok\"}\r\n\r\n\r\n@app.get(\"/raise\")\r\nasync def raise_route():\r\n    raise ValueError(\"oho\")\r\n\r\n ```\r\n\r\n2. install requirements: elastic-apm, fastapi, uvicorn\r\n3. run the app with an APM server running and define how to reach it:\r\n```\r\nELASTIC_APM_SERVER_URL=http://localhost:8200\r\nELASTIC_APM_SECRET_TOKEN=\r\nELASTIC_APM_SERVICE_NAME=my-service\r\nELASTIC_APM_ENVIRONMENT=test\r\n```\r\n\r\nrun it with: `uvicorn app:app`\r\n4. check if transactions are send and complete in APM, which is not the case\r\n\r\n\r\n**Environment (please complete the following information)**\r\n- OS: Debian bullseye and OS X\r\n- Python version: 3.10\r\n- Framework and version: elastic-apm-6.13.2 fastapi-0.88.0 starlette-0.22.0 uvicorn-0.20.0\r\n- APM Server version: 7.15.2 (local) and  8.4.2 on elastic-cloud.com\r\n- Agent version: 6.13.0, 6.13.1, 6.13.2\r\n\r\n\r\n**Additional context**\r\n\r\nIt seems that this happens since the change to the StarletteServerErrorMiddlewareInstrumentation done here https://github.com/elastic/apm-agent-python/pull/1674/files\r\n\r\n- `requirements.txt`:\r\n  <details>\r\n    <summary>Click to expand</summary>\r\n\r\n    ```\r\n  elastic-apm==6.13.2\r\n  fastapi==0.88.0\r\n  uvicorn==0.20.0\r\n    ```\r\n  </details>\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\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):\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 = 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):\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):\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):\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):\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], **kwargs):\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):\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):\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):\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):\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"}]}