We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
FastAPI: Calls to undefined routes result in 500
**Description**
When using the apm agent with FastAPI, calls to any undefined route results in a 500 internal server error. Without apm, 404 not found is returned correctly.

**To Reproduce**

Run the following simple FastAPI app:
```python
import uvicorn
from elasticapm.contrib.starlette import ElasticAPM, make_apm_client
from fastapi import FastAPI

app = FastAPI()

apm = make_apm_client(
    {
        "SERVICE_NAME": "undefined-route",
    }
)

app.add_middleware(ElasticAPM, client=apm)

@app.get("/hi")
async def hi():
    return "hi"

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

**Observed behavior**
Calling `http://localhost/bye` results in a 500 internal server error.

**Expected behavior**
Calling  `http://localhost/bye` results in a 404 not found error.

**Environment**

- OS:
  - Client: Windows
  - Server: Ubuntu
- Python version: `3.7.3`
- Framework and version: `fastapi==0.61.2`
- APM Server version: docker image `elasticsearch/elasticsearch:7.10.2`
- Agent version: `elastic-apm==master`

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