We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Crash on starlette middleware in mount endpoints (StaticFiles)
**Describe the bug**: Elastic middleware crash on access to static files

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

```python
from __future__ import annotations

import uvicorn
from elasticapm.contrib.starlette import ElasticAPM, make_apm_client
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles

app = FastAPI(routes=[])
apm_config = {
    "SERVICE_NAME": "testing",
    "ENABLED": True,
    "SERVER_URL": "http://localhost:8200",
    "CAPTURE_HEADERS": True,
    "CAPTURE_BODY": "all",
}
apm = make_apm_client(apm_config)
app.add_middleware(ElasticAPM, client=apm)
app.mount("/static",StaticFiles(directory="/"),name="static")


if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", debug=False)
```

The issue is happens when elastic running the function _get_route_name.
the middleware will try to loop over the routes where starlette will return None in some cases:
```python
  File "/home/syniex/.local/lib/python3.9/site-packages/elasticapm/contrib/starlette/__init__.py", line 233, in _get_route_name
    for route in routes:
                 └ None

TypeError: 'NoneType' object is not iterable

```
Starlette code that makes the issue:
```python
@property
    def routes(self) -> typing.List[BaseRoute]:
        return getattr(self.app, "routes", None)
```

--- 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 from typing import Dict, Optional
35 
36 import starlette
37 from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
38 from starlette.requests import Request
39 from starlette.responses import Response
40 from starlette.routing import Match, Mount
41 from starlette.types import ASGIApp
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.logging import get_logger
51 
52 logger = get_logger("elasticapm.errors.client")
53 
54 
55 def make_apm_client(config: Optional[Dict] = None, client_cls=Client, **defaults) -> Client:
56     """Builds ElasticAPM client.
57 
58     Args:
59         config (dict): Dictionary of Client configuration. All keys must be uppercase. See `elasticapm.conf.Config`.
60         client_cls (Client): Must be Client or its child.
61         **defaults: Additional parameters for Client. See `elasticapm.base.Client`
62 
63     Returns:
64         Client
65     """
66     if "framework_name" not in defaults:
67         defaults["framework_name"] = "starlette"
68         defaults["framework_version"] = starlette.__version__
69 
70     return client_cls(config, **defaults)
71 
72 
73 class ElasticAPM(BaseHTTPMiddleware):
74     """
75     Starlette / FastAPI middleware for Elastic APM capturing.
76 
77     >>> elasticapm = make_apm_client({
78         >>> 'SERVICE_NAME': 'myapp',
79         >>> 'DEBUG': True,
80         >>> 'SERVER_URL': 'http://localhost:8200',
81         >>> 'CAPTURE_HEADERS': True,
82         >>> 'CAPTURE_BODY': 'all'
83     >>> })
84 
85     >>> app.add_middleware(ElasticAPM, client=elasticapm)
86 
87     Pass an arbitrary APP_NAME and SECRET_TOKEN::
88 
89     >>> elasticapm = ElasticAPM(app, service_name='myapp', secret_token='asdasdasd')
90 
91     Pass an explicit client::
92 
93     >>> elasticapm = ElasticAPM(app, client=client)
94 
95     Automatically configure logging::
96 
97     >>> elasticapm = ElasticAPM(app, logging=True)
98 
99     Capture an exception::
100 
101     >>> try:
102     >>>     1 / 0
103     >>> except ZeroDivisionError:
104     >>>     elasticapm.capture_exception()
105 
106     Capture a message::
107 
108     >>> elasticapm.capture_message('hello, world!')
109     """
110 
111     def __init__(self, app: ASGIApp, client: Client):
112         """
113 
114         Args:
115             app (ASGIApp): Starlette app
116             client (Client): ElasticAPM Client
117         """
118         self.client = client
119 
120         if self.client.config.instrument and self.client.config.enabled:
121             elasticapm.instrumentation.control.instrument()
122 
123         super().__init__(app)
124 
125     async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
126         """Processes the whole request APM capturing.
127 
128         Args:
129             request (Request)
130             call_next (RequestResponseEndpoint): Next request process in Starlette.
131 
132         Returns:
133             Response
134         """
135         await self._request_started(request)
136 
137         try:
138             response = await call_next(request)
139             elasticapm.set_transaction_outcome(constants.OUTCOME.SUCCESS, override=False)
140         except Exception:
141             await self.capture_exception(
142                 context={"request": await get_data_from_request(request, self.client.config, constants.ERROR)}
143             )
144             elasticapm.set_transaction_result("HTTP 5xx", override=False)
145             elasticapm.set_transaction_outcome(constants.OUTCOME.FAILURE, override=False)
146             elasticapm.set_context({"status_code": 500}, "response")
147 
148             raise
149         else:
150             await self._request_finished(response)
151         finally:
152             self.client.end_transaction()
153 
154         return response
155 
156     async def capture_exception(self, *args, **kwargs):
157         """Captures your exception.
158 
159         Args:
160             *args:
161             **kwargs:
162         """
163         self.client.capture_exception(*args, **kwargs)
164 
165     async def capture_message(self, *args, **kwargs):
166         """Captures your message.
167 
168         Args:
169             *args: Whatever
170             **kwargs: Whatever
171         """
172         self.client.capture_message(*args, **kwargs)
173 
174     async def _request_started(self, request: Request):
175         """Captures the begin of the request processing to APM.
176 
177         Args:
178             request (Request)
179         """
180         # When we consume the body, we replace the streaming mechanism with
181         # a mocked version -- this workaround came from
182         # https://github.com/encode/starlette/issues/495#issuecomment-513138055
183         # and we call the workaround here to make sure that regardless of
184         # `capture_body` settings, we will have access to the body if we need it.
185         if self.client.config.capture_body != "off":
186             await get_body(request)
187 
188         if not self.client.should_ignore_url(request.url.path):
189             trace_parent = TraceParent.from_headers(dict(request.headers))
190             self.client.begin_transaction("request", trace_parent=trace_parent)
191 
192             await set_context(
193                 lambda: get_data_from_request(request, self.client.config, constants.TRANSACTION), "request"
194             )
195             transaction_name = self.get_route_name(request) or request.url.path
196             elasticapm.set_transaction_name("{} {}".format(request.method, transaction_name), override=False)
197 
198     async def _request_finished(self, response: Response):
199         """Captures the end of the request processing to APM.
200 
201         Args:
202             response (Response)
203         """
204         await set_context(
205             lambda: get_data_from_response(response, self.client.config, constants.TRANSACTION), "response"
206         )
207 
208         result = "HTTP {}xx".format(response.status_code // 100)
209         elasticapm.set_transaction_result(result, override=False)
210 
211     def get_route_name(self, request: Request) -> str:
212         app = request.app
213         scope = request.scope
214         routes = app.routes
215         route_name = self._get_route_name(scope, routes)
216 
217         # Starlette magically redirects requests if the path matches a route name with a trailing slash
218         # appended or removed. To not spam the transaction names list, we do the same here and put these
219         # redirects all in the same "redirect trailing slashes" transaction name
220         if not route_name and app.router.redirect_slashes and scope["path"] != "/":
221             redirect_scope = dict(scope)
222             if scope["path"].endswith("/"):
223                 redirect_scope["path"] = scope["path"][:-1]
224                 trim = True
225             else:
226                 redirect_scope["path"] = scope["path"] + "/"
227                 trim = False
228 
229             route_name = self._get_route_name(redirect_scope, routes)
230             if route_name is not None:
231                 route_name = route_name + "/" if trim else route_name[:-1]
232         return route_name
233 
234     def _get_route_name(self, scope, routes, route_name=None):
235         for route in routes:
236             match, child_scope = route.matches(scope)
237             if match == Match.FULL:
238                 route_name = route.path
239                 child_scope = {**scope, **child_scope}
240                 if isinstance(route, Mount):
241                     child_route_name = self._get_route_name(child_scope, route.routes, route_name)
242                     if child_route_name is None:
243                         route_name = None
244                     else:
245                         route_name += child_route_name
246                 return route_name
247             elif match == Match.PARTIAL and route_name is None:
248                 route_name = route.path
249
```
--- 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
@@ -237,7 +237,7 @@
             if match == Match.FULL:
                 route_name = route.path
                 child_scope = {**scope, **child_scope}
-                if isinstance(route, Mount):
+                if isinstance(route, Mount) and route.routes:
                     child_route_name = self._get_route_name(child_scope, route.routes, route_name)
                     if child_route_name is None:
                         route_name = None

{"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@@ -237,7 +237,7 @@\n             if match == Match.FULL:\n                 route_name = route.path\n                 child_scope = {**scope, **child_scope}\n-                if isinstance(route, Mount):\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", "issue": "Crash on starlette middleware in mount endpoints (StaticFiles)\n**Describe the bug**: Elastic middleware crash on access to static files\r\n\r\n**Environment (please complete the following information)**\r\n- OS: [e.g. Linux]\r\n- Python version: 3.9.4\r\n- Framework and version [e.g. Django 2.1]: FastAPI\r\n- APM Server version: 7.12.1\r\n- Agent version: 7.12.1\r\n\r\n```python\r\nfrom __future__ import annotations\r\n\r\nimport uvicorn\r\nfrom elasticapm.contrib.starlette import ElasticAPM, make_apm_client\r\nfrom fastapi import FastAPI\r\nfrom fastapi.staticfiles import StaticFiles\r\n\r\napp = FastAPI(routes=[])\r\napm_config = {\r\n    \"SERVICE_NAME\": \"testing\",\r\n    \"ENABLED\": True,\r\n    \"SERVER_URL\": \"http://localhost:8200\",\r\n    \"CAPTURE_HEADERS\": True,\r\n    \"CAPTURE_BODY\": \"all\",\r\n}\r\napm = make_apm_client(apm_config)\r\napp.add_middleware(ElasticAPM, client=apm)\r\napp.mount(\"/static\",StaticFiles(directory=\"/\"),name=\"static\")\r\n\r\n\r\nif __name__ == \"__main__\":\r\n    uvicorn.run(app, host=\"0.0.0.0\", debug=False)\r\n```\r\n\r\nThe issue is happens when elastic running the function _get_route_name.\r\nthe middleware will try to loop over the routes where starlette will return None in some cases:\r\n```python\r\n  File \"/home/syniex/.local/lib/python3.9/site-packages/elasticapm/contrib/starlette/__init__.py\", line 233, in _get_route_name\r\n    for route in routes:\r\n                 \u2514 None\r\n\r\nTypeError: 'NoneType' object is not iterable\r\n\r\n```\r\nStarlette code that makes the issue:\r\n```python\r\n@property\r\n    def routes(self) -> typing.List[BaseRoute]:\r\n        return getattr(self.app, \"routes\", None)\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\nfrom typing import Dict, Optional\n\nimport starlette\nfrom starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint\nfrom starlette.requests import Request\nfrom starlette.responses import Response\nfrom starlette.routing import Match, Mount\nfrom starlette.types import ASGIApp\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.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(BaseHTTPMiddleware):\n    \"\"\"\n    Starlette / FastAPI middleware for Elastic APM capturing.\n\n    >>> elasticapm = 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=elasticapm)\n\n    Pass an arbitrary APP_NAME and SECRET_TOKEN::\n\n    >>> elasticapm = ElasticAPM(app, service_name='myapp', secret_token='asdasdasd')\n\n    Pass an explicit client::\n\n    >>> elasticapm = ElasticAPM(app, client=client)\n\n    Automatically configure logging::\n\n    >>> elasticapm = ElasticAPM(app, logging=True)\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: Client):\n        \"\"\"\n\n        Args:\n            app (ASGIApp): Starlette app\n            client (Client): ElasticAPM Client\n        \"\"\"\n        self.client = client\n\n        if self.client.config.instrument and self.client.config.enabled:\n            elasticapm.instrumentation.control.instrument()\n\n        super().__init__(app)\n\n    async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:\n        \"\"\"Processes the whole request APM capturing.\n\n        Args:\n            request (Request)\n            call_next (RequestResponseEndpoint): Next request process in Starlette.\n\n        Returns:\n            Response\n        \"\"\"\n        await self._request_started(request)\n\n        try:\n            response = await call_next(request)\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        else:\n            await self._request_finished(response)\n        finally:\n            self.client.end_transaction()\n\n        return response\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        if not self.client.should_ignore_url(request.url.path):\n            trace_parent = TraceParent.from_headers(dict(request.headers))\n            self.client.begin_transaction(\"request\", trace_parent=trace_parent)\n\n            await set_context(\n                lambda: get_data_from_request(request, self.client.config, constants.TRANSACTION), \"request\"\n            )\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    async def _request_finished(self, response: Response):\n        \"\"\"Captures the end of the request processing to APM.\n\n        Args:\n            response (Response)\n        \"\"\"\n        await set_context(\n            lambda: get_data_from_response(response, self.client.config, constants.TRANSACTION), \"response\"\n        )\n\n        result = \"HTTP {}xx\".format(response.status_code // 100)\n        elasticapm.set_transaction_result(result, 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):\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\nfrom typing import Dict, Optional\n\nimport starlette\nfrom starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint\nfrom starlette.requests import Request\nfrom starlette.responses import Response\nfrom starlette.routing import Match, Mount\nfrom starlette.types import ASGIApp\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.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(BaseHTTPMiddleware):\n    \"\"\"\n    Starlette / FastAPI middleware for Elastic APM capturing.\n\n    >>> elasticapm = 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=elasticapm)\n\n    Pass an arbitrary APP_NAME and SECRET_TOKEN::\n\n    >>> elasticapm = ElasticAPM(app, service_name='myapp', secret_token='asdasdasd')\n\n    Pass an explicit client::\n\n    >>> elasticapm = ElasticAPM(app, client=client)\n\n    Automatically configure logging::\n\n    >>> elasticapm = ElasticAPM(app, logging=True)\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: Client):\n        \"\"\"\n\n        Args:\n            app (ASGIApp): Starlette app\n            client (Client): ElasticAPM Client\n        \"\"\"\n        self.client = client\n\n        if self.client.config.instrument and self.client.config.enabled:\n            elasticapm.instrumentation.control.instrument()\n\n        super().__init__(app)\n\n    async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:\n        \"\"\"Processes the whole request APM capturing.\n\n        Args:\n            request (Request)\n            call_next (RequestResponseEndpoint): Next request process in Starlette.\n\n        Returns:\n            Response\n        \"\"\"\n        await self._request_started(request)\n\n        try:\n            response = await call_next(request)\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        else:\n            await self._request_finished(response)\n        finally:\n            self.client.end_transaction()\n\n        return response\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        if not self.client.should_ignore_url(request.url.path):\n            trace_parent = TraceParent.from_headers(dict(request.headers))\n            self.client.begin_transaction(\"request\", trace_parent=trace_parent)\n\n            await set_context(\n                lambda: get_data_from_request(request, self.client.config, constants.TRANSACTION), \"request\"\n            )\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    async def _request_finished(self, response: Response):\n        \"\"\"Captures the end of the request processing to APM.\n\n        Args:\n            response (Response)\n        \"\"\"\n        await set_context(\n            lambda: get_data_from_response(response, self.client.config, constants.TRANSACTION), \"response\"\n        )\n\n        result = \"HTTP {}xx\".format(response.status_code // 100)\n        elasticapm.set_transaction_result(result, 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"}]}