Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added route value to scope #804

Closed
wants to merge 4 commits into from
Closed

Added route value to scope #804

wants to merge 4 commits into from

Conversation

victoraugustolls
Copy link

The purpose of this pr is to add the RAW value of a path (route) to the scope.

Recently I tried to get this value and it didn't seem possible. Don't know if I did it the best way possible, but it seems so.

The main reason (at least for me) for having this value available is to aggregate values based on each endpoint in metric systems (Microsoft Application Insights, New Relic, Datadog...).

Please let me know if I need to change something or if this is not desirable!
Thanks!

@victoraugustolls
Copy link
Author

Hi @tomchristie, would you mind helping me here, if possible?

starlette/routing.py Outdated Show resolved Hide resolved
@tomchristie
Copy link
Member

Needs some thinking about:

Do we need to track any Mount that might have been matched too? Probably, otherwise eg. you can't actually build up the matched path. Perhaps we need eg. scope["routes"] = <list of Mounts and Route instances>

@victoraugustolls
Copy link
Author

Hmm, and inside each of the matching routes can have a filed saying if it was a full match or partial match?

@victoraugustolls
Copy link
Author

@tomchristie I added the routes to the scope as a draft to what you purposed. If you like this solution, I would gladly add tests. I maintained the child_scope so that anyone using it won't have any problems, and this wouldn't become a breaking change.

starlette/routing.py Outdated Show resolved Hide resolved
@victoraugustolls
Copy link
Author

@tomchristie what are your thoughts here? Can I proceed writing tests with this solution? Or should I change something?

@victoraugustolls
Copy link
Author

@tomchristie any updates here?

@tomchristie
Copy link
Member

I'd rather we don't include {"full": None, "partial": []} here.

The routes entry ought to just be a list of BaseRoute instances, which will generally be zero or more Mount instances, and then a final Route instance.

@victoraugustolls
Copy link
Author

@tomchristie updated to what I understood about you comment!

@victoraugustolls
Copy link
Author

Hi @tomchristie and @florimondmanca! I see this pr was mentioned in many places!

Can we get this moving? I will update the PR when I wake up (it's pretty late here) but would like to know if you guys are ok with this approach!

As I see, with this list of routes, the user would need to iterate over the list and join the path values for the Mounts and finally the Route, is that right?

Also, if that's the case, should we save the final full route value too?

@victoraugustolls
Copy link
Author

PR updated @tomchristie @florimondmanca !

@victoraugustolls
Copy link
Author

Tagging @euri10 and @JayH5 also as I saw them doing the reviews in other PR's!

Sorry if this is too much.

@@ -557,17 +560,20 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:

partial = None

scope["routes"] = []
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, perhaps I'm misunderstanding the aim of this — assuming we want routes to contain all BaseRoute items (mounts, routes) that have matched while going through the routing system, it seems that currently it will only ever contain a single item, i.e. the first one that matched? In case of a route under a Mount-ed app, it will be the Mount (even if the mounted app is actually a Starlette app perhaps with its own router), and in case of a regular route then it will be that Route. Correct?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Basing my judgement off #804 (comment) here - Not entirely sure that's something that's actually possible at all, given the ASGI-interoperable design aspects of Starlette.

For example, if you have…

async def other_non_starlette_app(scope, receive, send):
    ...

app = Starlette(routes=[Mount("/subapp", other_non_starlette_app)])

Then in this example context using scope["routes"][-1] as a way to get the "route that actually responds to the request" might be a bit confusing — it's only guaranteed to be the last route within this app that handles the request at least partly.

Don't know if we want to play on words here, or if for the use case you're describing (manipulating route information for telemetry purposes) having partial route info in some cases and marking it as a limitation would be enough?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it seems that currently it will only ever contain a single item, i.e. the first one that matched?

Yes, you are right! I didn't proceed with the PR because wanted to discuss the best strategy with the maintainers before and wasn't sure this was it.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not entirely sure that's something that's actually possible at all, given the ASGI-interoperable design aspects of Starlette.

An example would be subapps on a FastAPI app? If so, I posted some code on a comment here that works by iterating the routes and searching for a match, but I don't think the problem you described would happen with FastAPI :/

@JayH5
Copy link
Member

JayH5 commented Sep 6, 2020

I think I understand the need to get the route used for a request, but I'm a bit unclear on exactly how the routes would be used, especially when there are multiple routes. Maybe some test cases would make that clearer?

@victoraugustolls
Copy link
Author

I think I understand the need to get the route used for a request, but I'm a bit unclear on exactly how the routes would be used, especially when there are multiple routes. Maybe some test cases would make that clearer?

What do you mean by multiple routes? Are you referring to having multiple Mounts and then finally a Route?

I didn't write test cases because I wanted to discuss the best strategy to implement this before doing so.

In my mind the ideal is getting a single value from scope that represents the full route. This means of the user have sub apps, sub routers and etc, the value would be there with the full route.

But at the same type, there is a problem here. As routes are only matched after middlewares,and I think this would be the most common place to use the route value, thinking of a way to add middlewares after routes were matched, would be a nice addition.

@victoraugustolls
Copy link
Author

victoraugustolls commented Sep 6, 2020

Just and example of what I do in a middleware today to get this value (it could be improved):

route = None
for r in request.app.router.routes:
    if r.matches(request.scope)[0] == Match.FULL:
        route = r
        break

complete_route = ""
while isinstance(route, Mount):
    complete_route += route.path
    scope = dict(request.scope)
    scope["path"] = scope.get("path").replace(route.path, "")
    matched = False
    for r in route.routes:
        if r.matches(scope)[0] == Match.FULL:
            route = r
            matched = True
            break

    if matched is False:
        route = None

complete_route += (
    route.path
    if route is not None
    else request.url.path
)

beniwohli added a commit to beniwohli/apm-agent-python that referenced this pull request Oct 28, 2020
Unfortunately, we need to do some matching here that Starlette does
too, but currently isn't exposed. If/when encode/starlette#804
is merged, we can re-use the route from there.

closes elastic#833
beniwohli added a commit to elastic/apm-agent-python that referenced this pull request Nov 17, 2020
* Starlette: use route name as transaction name if available

Unfortunately, we need to do some matching here that Starlette does
too, but currently isn't exposed. If/when encode/starlette#804
is merged, we can re-use the route from there.

closes #833

* detect trailing slash redirects from Starlette and give them a dedicated transaction name

* adapt original route for slash redirects
@cdvv7788
Copy link

Hi. Currently running into this issue. Any progress on this? Something that needs help?
About the discussion, instead of returning a list of routes, a single string that contains the whole original route would suffice most of the use cases here.

beniwohli added a commit to beniwohli/apm-agent-python that referenced this pull request Sep 14, 2021
* Starlette: use route name as transaction name if available

Unfortunately, we need to do some matching here that Starlette does
too, but currently isn't exposed. If/when encode/starlette#804
is merged, we can re-use the route from there.

closes elastic#833

* detect trailing slash redirects from Starlette and give them a dedicated transaction name

* adapt original route for slash redirects
@macieyng
Copy link

Hi @victoraugustolls 👋 Can you share the details behind closing this PR? Could be useful in open-telemetry/opentelemetry-python-contrib#1759. Currently OpenTelemetry handle this manually.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants