Starlette: Include routing information for middleware

Created on 22 Oct 2019  路  12Comments  路  Source: encode/starlette

Is there a way to obtain which route the request is taking in middleware? Right now, the only available thing is request.url.path, which doesn't give me a nice name for metrics.

I see in https://github.com/steinnes/timing-asgi/blob/master/timing_asgi/integrations/starlette.py that it basically just loops through all the routes to obtain the matching one. Is there a better way to do it?

Looping in that manner also runs into an issue where if there are multiple matching routes, like
/resource/default/1
/resource/1/1
it can't find the right one since they both match.

Most helpful comment

sure thing

"""The goal is to have the route name of the current request                                                                                                                                 
accessible anywhere by our loggers to help us contextualizing                                                                                                                                
when debugging though kibana or other centralized log visualizer                                                                                                                             
"""                                                                                                                                                                                          
from typing import Callable                                                                                                                                                                  
from typing import Optional                                                                                                                                                                  
from contextvars import ContextVar                                                                                                                                                           

from fastapi import APIRouter, FastAPI                                                                                                                                                       
from fastapi.routing import APIRoute                                                                                                                                                         
from starlette.requests import Request                                                                                                                                                       
from starlette.responses import Response                                                                                                                                                     


_route_name_ctx_var: ContextVar[Optional[str]] = ContextVar("route_name", default=None)                                                                                                      


def get_route_name() -> Optional[str]:                                                                                                                                                       
    return _route_name_ctx_var.get()                                                                                                                                                         


class LoggedRoute(APIRoute):                                                                                                                                                                 
    def get_route_handler(self) -> Callable:                                                                                                                                                 
        original_route_handler = super().get_route_handler()                                                                                                                                 
        route_name = self.name                                                                                                                                                               

        # we import here to avoid the circular dependencies                                                                                                                                  
        from app.logging import get_logger                                                                                                                                                   

        async def custom_route_handler(request: Request) -> Response:                                                                                                                        

            _route_name_ctx_var.set(route_name)                                                                                                                                              

            app_logger = get_logger()                                                                                                                                                        
            app_logger.info({"step": "start"})                                                                                                                                               

            response: Response = await original_route_handler(request)                                                                                                                       

            app_logger.info({"step": "end", "status_code": response.status_code})                                                                                                            

            return response                                                                                                                                                                  

        return custom_route_handler              

and then I use it like this:

from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.engine import Connection

from app.database import get_connection
from app.logged_route import LoggedRoute


router = APIRouter(route_class=LoggedRoute)

All 12 comments

I got the same problem as @cypai with AuthenticationMiddleware. There is only request.scope["path"] and request.scope["raw_path"] available and I've to make quite some regex to handle the route properly.

Is it possible to build the Route() before *Middleware is called so one can get the route_name and other useful informations in the middleware? Maybe @cypai and I are both missing something?

So, the routing only occurs after the middleware handling.

I think options here are:

  • We could potentially add some public API on the application so that it's easier to premptively resolve the request routing? Ideally we ensure that can be cached, so that the lookup only occurs once.
  • We could add the ability to have some kinds of middleware run after the request routing. (Probably not super keen on that.)

Thanks for your answer @tomchristie. I don't know Starlette enough to know which solution would be best. I know that in some other frameworks, there is usually some Events mechanisms with some Listeners attached to it.

If I can be of any help, let me know, we use Starlette in production in my company, so it's critical for us.

@tomchristie why the second option not something you're keen on ? (honest question, I have no opinion)
but yes I've run into this issue because I was trying to put in a ContextVars the route name, so that my logger can get this information

right now a workaround I've found (as I'm using fastapi and not startlette directly, so not sure how much it applies on startette) is subclassing like here https://fastapi.tiangolo.com/tutorial/custom-request-and-route/#custom-apiroute-class-in-a-router and there I can access the routing information I need before/after request

@allan-simon Can you share the code of your workaround?

sure thing

"""The goal is to have the route name of the current request                                                                                                                                 
accessible anywhere by our loggers to help us contextualizing                                                                                                                                
when debugging though kibana or other centralized log visualizer                                                                                                                             
"""                                                                                                                                                                                          
from typing import Callable                                                                                                                                                                  
from typing import Optional                                                                                                                                                                  
from contextvars import ContextVar                                                                                                                                                           

from fastapi import APIRouter, FastAPI                                                                                                                                                       
from fastapi.routing import APIRoute                                                                                                                                                         
from starlette.requests import Request                                                                                                                                                       
from starlette.responses import Response                                                                                                                                                     


_route_name_ctx_var: ContextVar[Optional[str]] = ContextVar("route_name", default=None)                                                                                                      


def get_route_name() -> Optional[str]:                                                                                                                                                       
    return _route_name_ctx_var.get()                                                                                                                                                         


class LoggedRoute(APIRoute):                                                                                                                                                                 
    def get_route_handler(self) -> Callable:                                                                                                                                                 
        original_route_handler = super().get_route_handler()                                                                                                                                 
        route_name = self.name                                                                                                                                                               

        # we import here to avoid the circular dependencies                                                                                                                                  
        from app.logging import get_logger                                                                                                                                                   

        async def custom_route_handler(request: Request) -> Response:                                                                                                                        

            _route_name_ctx_var.set(route_name)                                                                                                                                              

            app_logger = get_logger()                                                                                                                                                        
            app_logger.info({"step": "start"})                                                                                                                                               

            response: Response = await original_route_handler(request)                                                                                                                       

            app_logger.info({"step": "end", "status_code": response.status_code})                                                                                                            

            return response                                                                                                                                                                  

        return custom_route_handler              

and then I use it like this:

from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.engine import Connection

from app.database import get_connection
from app.logged_route import LoggedRoute


router = APIRouter(route_class=LoggedRoute)

@allan-simon Thanks. What is get_route_name used for?

@shizidushu here I will not be able to simply copy paste you code, but here basically the usage:

I've configured all the loggers to have a JSON Formatter in order for my logs to be then integrated into a centralized log system (ELK stack)
in addition to this all the loggers have also a logfilter which add in the json , the field "route_name" (and other field , like user_id , request_id etc. ) so that if anywhere in the code I do "logger.info("hey") " I will be able to know it was called in the context of a given route

the goal is that this way it's easy to make statistics by api endpoint name

@allan-simon Thanks for your explanation. So, in your case, it is used to expose the route context ("route_name") to logger. It is an inspiring idea!

Question for the group here... tagging onto this thread because I think it may be related:

Am I correct in thinking route discovery is currently the best way to handle a pipeline of middleware for a given route or group of routes?

A little backdrop... I am thinking about the equivalent of a policies.js file in nodejs, where you may build a pipeline of checks for a given request:

admin_policies = [https_redirect, is_logged_in, is_admin]
user_policies = [https_redirect, is_logged_in]

My understanding is that policies (which are I think identical in implementation to middleware, though serve a subtly different purpose) are not directly supported in starlette, which is fine. That said, it might be nice to be able to assign functions (at the router level) assign those functions/routes to those policies so anyone who hits that route goes through that same pipeline.

I've found this kind of structure quite nice, and while starlette is meant to be lightweight and may expect something else to plug into it (which is fine), I'm curious if anyone has approached something similar... and if so, how?

EDIT: Please let me know if you think this question is more appropriate as its own issue.

Just wanted to add my voice to this issue. We want to log response times per endpoint. As this is done at the end of the request, it is perfectly fine for us to not know the endpoint name until the request has been resolved.

The workaround posted by @allan-simon might work, but I would rather handle response times in a middleware than in the routes themselves.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Immortalin picture Immortalin  路  3Comments

jordic picture jordic  路  6Comments

cw-andrews picture cw-andrews  路  4Comments

koddr picture koddr  路  6Comments

kouohhashi picture kouohhashi  路  3Comments