We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Consider using route information instead of URL for Starlette transactions
We're slowly started using Starlette for some smaller applications due to its simplicity and async capabilities. We also happen to use Elastic APM for our monitoring, but we've found that the agent works quite different from other agents, such as Django or Flask.

The default for Django, is "HTTP Method + path.to.view". This can be changed for Django 2.2+  using the `django-transaction-name-from-route` setting, to instead use the route that was matched against. For example `GET /users/<int:user_id>/`.

For Flask, the default seems to be the that of method + route, e.g.: `GET /users/<int:user_id/`.

For Starlette however, information about the matched route is not actually available on the request, and for that reason (I assume), the full URL is instead used as the transaction name.
In the example above, it would mean that every time a user with a different URL was hit, it would generate two separate transaction entries. For some of our views, that would mean 1+ milllion transaction names for the same view.

However, it is somewhat possible to extract the route by mimicking what Starlette itself does. On the request, the Application itself is available, and from that route router + routes. This means it's possible to loop over them, and match their regular expression against the current request.

1) I realise this isn't pretty in any way, shape or form - but it would make instrumentation of Starlette applications a lot more useable
2) I also realise it's backwards incompatible - but perhaps this could be solved in the same way that it's done with Django?

What would your thought be on such a change?

For us at our workplace, we've actually have one major of our main applications ported to Starlette, but once we saw what was happening to the transaction names, we had to revert the deployment. We'd love to have it running, so we'd probably be interested in providing a patch for it as well.

**EDIT**
Just wanted to add this here, since it's a discussion of making this type of information available in the middleware system: https://github.com/encode/starlette/issues/685
It also links another repo for timing of ASGI that basically does what I describe here, by looping over all routes and comparing them to the current URL.

--- 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.types import ASGIApp
39 
40 import elasticapm
41 import elasticapm.instrumentation.control
42 from elasticapm.base import Client
43 from elasticapm.conf import constants
44 from elasticapm.contrib.asyncio.traces import set_context
45 from elasticapm.contrib.starlette.utils import get_data_from_request, get_data_from_response
46 from elasticapm.utils.disttracing import TraceParent
47 from elasticapm.utils.logging import get_logger
48 
49 logger = get_logger("elasticapm.errors.client")
50 
51 
52 def make_apm_client(config: dict, client_cls=Client, **defaults) -> Client:
53     """Builds ElasticAPM client.
54 
55     Args:
56         config (dict): Dictionary of Client configuration. All keys must be uppercase. See `elasticapm.conf.Config`.
57         client_cls (Client): Must be Client or its child.
58         **defaults: Additional parameters for Client. See `elasticapm.base.Client`
59 
60     Returns:
61         Client
62     """
63     if "framework_name" not in defaults:
64         defaults["framework_name"] = "starlette"
65         defaults["framework_version"] = starlette.__version__
66 
67     return client_cls(config, **defaults)
68 
69 
70 class ElasticAPM(BaseHTTPMiddleware):
71     """
72     Starlette / FastAPI middleware for Elastic APM capturing.
73 
74     >>> elasticapm = make_apm_client({
75         >>> 'SERVICE_NAME': 'myapp',
76         >>> 'DEBUG': True,
77         >>> 'SERVER_URL': 'http://localhost:8200',
78         >>> 'CAPTURE_HEADERS': True,
79         >>> 'CAPTURE_BODY': 'all'
80     >>> })
81 
82     >>> app.add_middleware(ElasticAPM, client=elasticapm)
83 
84     Pass an arbitrary APP_NAME and SECRET_TOKEN::
85 
86     >>> elasticapm = ElasticAPM(app, service_name='myapp', secret_token='asdasdasd')
87 
88     Pass an explicit client::
89 
90     >>> elasticapm = ElasticAPM(app, client=client)
91 
92     Automatically configure logging::
93 
94     >>> elasticapm = ElasticAPM(app, logging=True)
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: Client):
109         """
110 
111         Args:
112             app (ASGIApp): Starlette app
113             client (Client): ElasticAPM Client
114         """
115         self.client = client
116 
117         if self.client.config.instrument:
118             elasticapm.instrumentation.control.instrument()
119 
120         super().__init__(app)
121 
122     async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
123         """Processes the whole request APM capturing.
124 
125         Args:
126             request (Request)
127             call_next (RequestResponseEndpoint): Next request process in Starlette.
128 
129         Returns:
130             Response
131         """
132         await self._request_started(request)
133 
134         try:
135             response = await call_next(request)
136             elasticapm.set_transaction_outcome(constants.OUTCOME.SUCCESS, override=False)
137         except Exception:
138             await self.capture_exception(
139                 context={"request": await get_data_from_request(request, self.client.config, constants.ERROR)}
140             )
141             elasticapm.set_transaction_result("HTTP 5xx", override=False)
142             elasticapm.set_transaction_outcome(constants.OUTCOME.FAILURE, override=False)
143             elasticapm.set_context({"status_code": 500}, "response")
144 
145             raise
146         else:
147             await self._request_finished(response)
148         finally:
149             self.client.end_transaction()
150 
151         return response
152 
153     async def capture_exception(self, *args, **kwargs):
154         """Captures your exception.
155 
156         Args:
157             *args:
158             **kwargs:
159         """
160         self.client.capture_exception(*args, **kwargs)
161 
162     async def capture_message(self, *args, **kwargs):
163         """Captures your message.
164 
165         Args:
166             *args: Whatever
167             **kwargs: Whatever
168         """
169         self.client.capture_message(*args, **kwargs)
170 
171     async def _request_started(self, request: Request):
172         """Captures the begin of the request processing to APM.
173 
174         Args:
175             request (Request)
176         """
177         if not self.client.should_ignore_url(request.url.path):
178             trace_parent = TraceParent.from_headers(dict(request.headers))
179             self.client.begin_transaction("request", trace_parent=trace_parent)
180 
181             await set_context(
182                 lambda: get_data_from_request(request, self.client.config, constants.TRANSACTION), "request"
183             )
184             elasticapm.set_transaction_name("{} {}".format(request.method, request.url.path), override=False)
185 
186     async def _request_finished(self, response: Response):
187         """Captures the end of the request processing to APM.
188 
189         Args:
190             response (Response)
191         """
192         await set_context(
193             lambda: get_data_from_response(response, self.client.config, constants.TRANSACTION), "response"
194         )
195 
196         result = "HTTP {}xx".format(response.status_code // 100)
197         elasticapm.set_transaction_result(result, override=False)
198
```
--- 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
@@ -35,6 +35,7 @@
 from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
 from starlette.requests import Request
 from starlette.responses import Response
+from starlette.routing import Match
 from starlette.types import ASGIApp
 
 import elasticapm
@@ -181,7 +182,8 @@
             await set_context(
                 lambda: get_data_from_request(request, self.client.config, constants.TRANSACTION), "request"
             )
-            elasticapm.set_transaction_name("{} {}".format(request.method, request.url.path), override=False)
+            transaction_name = self.get_route_name(request) or request.url.path
+            elasticapm.set_transaction_name("{} {}".format(request.method, transaction_name), override=False)
 
     async def _request_finished(self, response: Response):
         """Captures the end of the request processing to APM.
@@ -195,3 +197,34 @@
 
         result = "HTTP {}xx".format(response.status_code // 100)
         elasticapm.set_transaction_result(result, override=False)
+
+    def get_route_name(self, request: Request) -> str:
+        route_name = None
+        app = request.app
+        scope = request.scope
+        routes = app.routes
+
+        for route in routes:
+            match, _ = route.matches(scope)
+            if match == Match.FULL:
+                route_name = route.path
+                break
+            elif match == Match.PARTIAL and route_name is None:
+                route_name = route.path
+        # Starlette magically redirects requests if the path matches a route name with a trailing slash
+        # appended or removed. To not spam the transaction names list, we do the same here and put these
+        # redirects all in the same "redirect trailing slashes" transaction name
+        if not route_name and app.router.redirect_slashes and scope["path"] != "/":
+            redirect_scope = dict(scope)
+            if scope["path"].endswith("/"):
+                redirect_scope["path"] = scope["path"][:-1]
+                trim = True
+            else:
+                redirect_scope["path"] = scope["path"] + "/"
+                trim = False
+            for route in routes:
+                match, _ = route.matches(redirect_scope)
+                if match != Match.NONE:
+                    route_name = route.path + "/" if trim else route.path[:-1]
+                    break
+        return route_name

{"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@@ -35,6 +35,7 @@\n from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint\n from starlette.requests import Request\n from starlette.responses import Response\n+from starlette.routing import Match\n from starlette.types import ASGIApp\n \n import elasticapm\n@@ -181,7 +182,8 @@\n             await set_context(\n                 lambda: get_data_from_request(request, self.client.config, constants.TRANSACTION), \"request\"\n             )\n-            elasticapm.set_transaction_name(\"{} {}\".format(request.method, request.url.path), override=False)\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@@ -195,3 +197,34 @@\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+        route_name = None\n+        app = request.app\n+        scope = request.scope\n+        routes = app.routes\n+\n+        for route in routes:\n+            match, _ = route.matches(scope)\n+            if match == Match.FULL:\n+                route_name = route.path\n+                break\n+            elif match == Match.PARTIAL and route_name is None:\n+                route_name = route.path\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+            for route in routes:\n+                match, _ = route.matches(redirect_scope)\n+                if match != Match.NONE:\n+                    route_name = route.path + \"/\" if trim else route.path[:-1]\n+                    break\n+        return route_name\n", "issue": "Consider using route information instead of URL for Starlette transactions\nWe're slowly started using Starlette for some smaller applications due to its simplicity and async capabilities. We also happen to use Elastic APM for our monitoring, but we've found that the agent works quite different from other agents, such as Django or Flask.\r\n\r\nThe default for Django, is \"HTTP Method + path.to.view\". This can be changed for Django 2.2+  using the `django-transaction-name-from-route` setting, to instead use the route that was matched against. For example `GET /users/<int:user_id>/`.\r\n\r\nFor Flask, the default seems to be the that of method + route, e.g.: `GET /users/<int:user_id/`.\r\n\r\nFor Starlette however, information about the matched route is not actually available on the request, and for that reason (I assume), the full URL is instead used as the transaction name.\r\nIn the example above, it would mean that every time a user with a different URL was hit, it would generate two separate transaction entries. For some of our views, that would mean 1+ milllion transaction names for the same view.\r\n\r\nHowever, it is somewhat possible to extract the route by mimicking what Starlette itself does. On the request, the Application itself is available, and from that route router + routes. This means it's possible to loop over them, and match their regular expression against the current request.\r\n\r\n1) I realise this isn't pretty in any way, shape or form - but it would make instrumentation of Starlette applications a lot more useable\r\n2) I also realise it's backwards incompatible - but perhaps this could be solved in the same way that it's done with Django?\r\n\r\nWhat would your thought be on such a change?\r\n\r\nFor us at our workplace, we've actually have one major of our main applications ported to Starlette, but once we saw what was happening to the transaction names, we had to revert the deployment. We'd love to have it running, so we'd probably be interested in providing a patch for it as well.\r\n\r\n**EDIT**\r\nJust wanted to add this here, since it's a discussion of making this type of information available in the middleware system: https://github.com/encode/starlette/issues/685\r\nIt also links another repo for timing of ASGI that basically does what I describe here, by looping over all routes and comparing them to the current URL.\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.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_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:\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        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            elasticapm.set_transaction_name(\"{} {}\".format(request.method, request.url.path), 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", "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\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_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:\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        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        route_name = None\n        app = request.app\n        scope = request.scope\n        routes = app.routes\n\n        for route in routes:\n            match, _ = route.matches(scope)\n            if match == Match.FULL:\n                route_name = route.path\n                break\n            elif match == Match.PARTIAL and route_name is None:\n                route_name = route.path\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            for route in routes:\n                match, _ = route.matches(redirect_scope)\n                if match != Match.NONE:\n                    route_name = route.path + \"/\" if trim else route.path[:-1]\n                    break\n        return route_name\n", "path": "elasticapm/contrib/starlette/__init__.py"}]}