asgi-server-timing-middleware

Overview

ASGI Server-Timing middleware

An ASGI middleware that wraps the excellent yappi profiler to let you measure the execution time of any function or coroutine in the context of an HTTP request, and return it as a standard Server-Timing HTTP header.

Sample configurations

Here are some example configurations for various frameworks and libraries. Feel free to combine them as needed.

FastAPI

fastapi_app.add_middleware(ServerTimingMiddleware, calls_to_track={
	"1deps": (fastapi.routing.solve_dependencies,),
	"2main": (fastapi.routing.run_endpoint_function,),
	"3valid": (pydantic.fields.ModelField.validate,),
	"4encode": (fastapi.encoders.jsonable_encoder,),
	"5render": (
		fastapi.responses.JSONResponse.render,
		fastapi.responses.ORJSONResponse.render,
	),
})

Starlette

from starlette.middleware import Middleware

middleware = [
  Middleware(ServerTimingMiddleware, calls_to_track={
	  # TODO: ...
  }),
]

starlette_app = Starlette(routes=routes, middleware=middleware)

SQLAlchemy

fastapi_app.add_middleware(ServerTimingMiddleware, calls_to_track={
	"db_exec": (sqlalchemy.engine.base.Engine.execute,),
	"db_fetch": (
		sqlalchemy.engine.ResultProxy.fetchone,
		sqlalchemy.engine.ResultProxy.fetchmany,
		sqlalchemy.engine.ResultProxy.fetchall,
	),
})

More Frameworks

Feel free to submit PRs containing examples for more libraries and ASGI frameworks.

Caveats

  • Only the end-to-end time is reported for both functions and coroutines, so it's not possible to tell from the metrics when a coroutine took a long time because the event loop thread got stalled (though asyncio's debug mode can help).
  • The profiler's memory is not freed over time, and only gets cleared when it exceeds a given threshold (50MB by default). When memory gets cleared, data collected for all ongoing requests is lost, so the timing for these will be incorrect.
  • Executing the same task multiple times in parallel (such as with asyncio.gather()) will report the duration as if they had been executed sequentially.
  • The minimum version of Python supported is 3.7, since this middleware makes use of PEP 567 context variables to track which function call belongs to which request, and the Python 3.6 backport doesn't have asyncio support.

Special Thanks

  • Sümer Cip (@sumerc), for creating and maintaininng yappi, as well as being very responsive and open to adding all the new features needed to make this work.
  • David Montague (@dmontagu) for his involvement in shaping this middleware at every step of the way.
You might also like...
A Python application for tracking, reporting on timing and complexity in Python code
A Python application for tracking, reporting on timing and complexity in Python code

A command-line application for tracking, reporting on complexity of Python tests and applications. wily [a]: quick to think of things, having a very g

A Python utility belt containing simple tools, a stdlib like feel, and extra batteries. Hashing, Caching, Timing, Progress, and more made easy!
A Python utility belt containing simple tools, a stdlib like feel, and extra batteries. Hashing, Caching, Timing, Progress, and more made easy!

Ubelt is a small library of robust, tested, documented, and simple functions that extend the Python standard library. It has a flat API that all behav

A Python utility belt containing simple tools, a stdlib like feel, and extra batteries. Hashing, Caching, Timing, Progress, and more made easy!
A Python utility belt containing simple tools, a stdlib like feel, and extra batteries. Hashing, Caching, Timing, Progress, and more made easy!

Ubelt is a small library of robust, tested, documented, and simple functions that extend the Python standard library. It has a flat API that all behav

CPSPEC is an astrophysical data reduction software for timing

CPSPEC manual Introduction CPSPEC is an astrophysical data reduction software for timing. Various timing properties, such as power spectra and cross s

A task scheduler with task scheduling, timing and task completion time tracking functions

A task scheduler with task scheduling, timing and task completion time tracking functions. Could be helpful for time management in daily life.

The little ASGI framework that shines. ?
The little ASGI framework that shines. ?

✨ The little ASGI framework that shines. ✨ Documentation: https://www.starlette.io/ Community: https://discuss.encode.io/c/starlette Starlette Starlet

ASGI support for the Tartiflette GraphQL engine
ASGI support for the Tartiflette GraphQL engine

tartiflette-asgi is a wrapper that provides ASGI support for the Tartiflette Python GraphQL engine. It is ideal for serving a GraphQL API over HTTP, o

ASGI specification and utilities

asgiref ASGI is a standard for Python asynchronous web apps and servers to communicate with each other, and positioned as an asynchronous successor to

AWS Lambda & API Gateway support for ASGI

Mangum Mangum is an adapter for using ASGI applications with AWS Lambda & API Gateway. It is intended to provide an easy-to-use, configurable wrapper

ASGI support for the Tartiflette GraphQL engine
ASGI support for the Tartiflette GraphQL engine

tartiflette-asgi is a wrapper that provides ASGI support for the Tartiflette Python GraphQL engine. It is ideal for serving a GraphQL API over HTTP, o

A framework-agnostic library for testing ASGI web applications

async-asgi-testclient Async ASGI TestClient is a library for testing web applications that implements ASGI specification (version 2 and 3). The motiva

Drop-in MessagePack support for ASGI applications and frameworks
Drop-in MessagePack support for ASGI applications and frameworks

msgpack-asgi msgpack-asgi allows you to add automatic MessagePack content negotiation to ASGI applications (Starlette, FastAPI, Quart, etc.), with a s

The little ASGI framework that shines. 🌟
The little ASGI framework that shines. 🌟

✨ The little ASGI framework that shines. ✨ Documentation: https://www.starlette.io/ Community: https://discuss.encode.io/c/starlette Starlette Starlet

Inject an ID into every log message from a Django request. ASGI compatible, integrates with Sentry, and works with Celery

Django GUID Now with ASGI support! Django GUID attaches a unique correlation ID/request ID to all your log outputs for every request. In other words,

Fast⚡, simple and light💡weight ASGI micro🔬 web🌏-framework for Python🐍.

NanoASGI Asynchronous Python Web Framework NanoASGI is a fast ⚡ , simple and light 💡 weight ASGI micro 🔬 web 🌏 -framework for Python 🐍 . It is dis

Request ID propagation for ASGI apps

ASGI Correlation ID middleware Middleware for loading and receiving correlation IDs from request HTTP headers, and making them available in applicatio

Bablyon 🐍 A small ASGI web framework

A small ASGI web framework that you can make asynchronous web applications using uvicorn with using few lines of code

Light, Flexible and Extensible ASGI API framework
Light, Flexible and Extensible ASGI API framework

Starlite Starlite is a light and flexible ASGI API framework. Using Starlette and pydantic as foundations. Check out the Starlite documentation 📚 Cor

WSGI middleware for sessions and caching

Cache and Session Library About Beaker is a web session and general caching library that includes WSGI middleware for use in web applications. As a ge

Comments
  • SQLAlchemy async

    SQLAlchemy async

    Hi - would someone know how to profile async SQLAlchemy operations with this?

    This works in the sense that I get to see something, but the timing is always ~1ms, maybe because that's how quickly the initial call to the coroutine returns?

      "db_async_exec": (sqlalchemy.ext.asyncio.AsyncSession.execute,),
    

    I also tried to add equivalents to the ResultProxy examples, but these are not correct, don't show up in the timings at all, probably these are just wrong functions.

      "db_async_fetch": (
          sqlalchemy.ext.asyncio.AsyncResult.fetchone,
          sqlalchemy.ext.asyncio.AsyncResult.fetchmany,
      )```
    opened by antont 4
  • `pydantic.fields.ModelField.validate` doesn't work anymore

    `pydantic.fields.ModelField.validate` doesn't work anymore

    In the README, it says to use fastapi.routing.ModelField.validate in order to profile how long it takes to validate the response.

    No matter which function calls I ask it to profile I'm able to get yappi to return a profile for the 3valid step.

        "3valid": (
            fastapi.routing.BaseModel.validate,
            fastapi.routing.ModelField.validate,
            pydantic.fields.BaseModel.validate,
            pydantic.fields.ModelField.validate
        ),
    

    I can confirm by entering a debugger that these functions are being called, it's just that yappi isn't profiling them. I'm not entirely sure why.

    I know you haven't looked in this in a while and you did so much research. to get this working in the first place. It's greatly appreciated!

    Here's a repro:

    import fastapi
    import pydantic
    
    
    from contextvars import ContextVar
    from typing import Dict, List, Tuple, Callable
    import re
    
    import yappi
    from fastapi import FastAPI
    from yappi import YFuncStats
    
    _yappi_ctx_tag: ContextVar[int] = ContextVar("_yappi_ctx_tag", default=-1)
    
    
    def _get_context_tag() -> int:
        return _yappi_ctx_tag.get()
    
    
    class ServerTimingMiddleware:
        """Timing middleware for ASGI HTTP applications
    
        The resulting profiler data will be returned through the standard
        `Server-Timing` header for all requests.
    
        Args:
            app (ASGI v3 callable): An ASGI application
    
            calls_to_track (Dict[str,Tuple[Callable]]): A dict of functions
                keyed by desired output metric name.
    
                Metric names must consist of a single rfc7230 token
    
            max_profiler_mem (int): Memory threshold (in bytes) at which yappi's
                profiler memory gets cleared.
    
        .. _Server-Timing sepcification:
            https://w3c.github.io/server-timing/#the-server-timing-header-field
        """
    
        def __init__(
            self, app, calls_to_track: Dict[str, Tuple[Callable]], max_profiler_mem: int = 50_000_000
        ):
            for metric_name, profiled_functions in calls_to_track.items():
                if len(metric_name) == 0:
                    raise ValueError("A Server-Timing metric name cannot be empty")
    
                # https://httpwg.org/specs/rfc7230.html#rule.token.separators
                # USASCII (7 bits), only visible characters (no non-printables or space), no double-quote or delimiter
                if (
                    not metric_name.isascii()
                    or not metric_name.isprintable()
                    or re.search(r'[ "(),/:;<=>?@\[\\\]{}]', metric_name) is not None
                ):
                    raise ValueError(
                        '"{}" contains an invalid character for a Server-Timing metric name'.format(
                            metric_name
                        )
                    )
    
                if not all(callable(profiled) for profiled in profiled_functions):
                    raise TypeError(
                        'One of the targeted functions for key "{}" is not a function'.format(
                            metric_name
                        )
                    )
    
            self.app = app
            self.calls_to_track = {
                name: list(tracked_funcs) for name, tracked_funcs in calls_to_track.items()
            }
            self.max_profiler_mem = max_profiler_mem
    
            yappi.set_tag_callback(_get_context_tag)
            yappi.set_clock_type("wall")
            yappi.set_context_backend("greenlet")
            yappi.start()
    
        async def __call__(self, scope, receive, send):
            ctx_tag = id(scope)
            _yappi_ctx_tag.set(ctx_tag)
    
            def wrapped_send(response):
                if response["type"] == "http.response.start":
                    tracked_stats: Dict[str, YFuncStats] = {
                        name: yappi.get_func_stats(
                            filter=dict(tag=ctx_tag),
                            filter_callback=lambda x: yappi.func_matches(x, tracked_funcs),
                        )
                        for name, tracked_funcs in self.calls_to_track.items()
                    }
    
                    # NOTE (sm15): Might need to be altered to account for various edge-cases
                    timing_ms = {
                        name: sum(x.ttot for x in stats) * 1000
                        for name, stats in tracked_stats.items()
                        if not stats.empty()
                    }
    
                    server_timing = ",".join(
                        [f"{name};dur={duration_ms:.3f}" for name, duration_ms in timing_ms.items()]
                    ).encode("ascii")
    
                    if server_timing:
                        # FIXME: Doesn't check if a server-timing header is already set
                        response["headers"].append([b"server-timing", server_timing])
    
                    if yappi.get_mem_usage() >= self.max_profiler_mem:
                        yappi.clear_stats()
    
                return send(response)
    
            await self.app(scope, receive, wrapped_send)
    
    
    
    track: Dict[str, Tuple[Callable, ...]] = {
        "1deps": (fastapi.routing.solve_dependencies,),
        "2main": (fastapi.routing.run_endpoint_function,),
        "3valid": (
            fastapi.routing.BaseModel.validate,
            fastapi.routing.ModelField.validate,
            pydantic.main.BaseModel.validate,
            pydantic.fields.ModelField.validate
        ),
        "4encode": (fastapi.encoders.jsonable_encoder,),
        "5render": (
            fastapi.responses.JSONResponse.render,
            fastapi.responses.ORJSONResponse.render,
        ),
    }
    app = FastAPI()
    app.add_middleware(ServerTimingMiddleware, calls_to_track=track)
    
    
    class Item(pydantic.main.BaseModel):
        name: int = pydantic.Field(description="name")
        val: int = pydantic.Field(description="val")
    
    
    @app.get("/", response_model=List[Item])
    def test():
        resp = [{"name": x, "val": x} for x in range(10000)]
        return resp
    
    

    And here's the result from hitting localhost:8000/ image

    opened by traviscook21 3
  • Cython functions aren't supported because of the way `inspect.isfunction` works

    Cython functions aren't supported because of the way `inspect.isfunction` works

    inspect.isfunction returns False for Cython functions (see this discussion).

    As a result, tracking pydantic.fields.ModelField.validate for pydantic==1.7.3 is impossible.

    opened by wxd 4
  • Create a setup.py script

    Create a setup.py script

    I don't really have much experience in the way of packaging and publishing python modules, but that's probably something I should get around doing if I want people to be able to use this.

    opened by sm-Fifteen 3
Owner
null
JSON-RPC server based on fastapi

Description JSON-RPC server based on fastapi: https://fastapi.tiangolo.com Motivation Autogenerated OpenAPI and Swagger (thanks to fastapi) for JSON-R

null 199 Dec 30, 2022
An extension for GINO to support Starlette server.

gino-starlette Introduction An extension for GINO to support starlette server. Usage The common usage looks like this: from starlette.applications imp

GINO Community 75 Dec 8, 2022
A Jupyter server based on FastAPI (Experimental)

jupyverse is experimental and should not be used in place of jupyter-server, which is the official Jupyter server.

Jupyter Server 122 Dec 27, 2022
OpenAPI generated FastAPI server

OpenAPI generated FastAPI server This Python package is automatically generated by the OpenAPI Generator project: API version: 1.0.0 Build package: or

microbo 1 Oct 31, 2021
Mnist API server w/ FastAPI

Mnist API server w/ FastAPI

Jinwoo Park (Curt) 8 Feb 8, 2022
ASGI middleware to record and emit timing metrics (to something like statsd)

timing-asgi This is a timing middleware for ASGI, useful for automatic instrumentation of ASGI endpoints. This was developed at GRID for use with our

Steinn Eldjárn Sigurðarson 99 Nov 21, 2022
ASGI middleware for authentication, rate limiting, and building CRUD endpoints.

Piccolo API Utilities for easily exposing Piccolo models as REST endpoints in ASGI apps, such as Starlette and FastAPI. Includes a bunch of useful ASG

null 81 Dec 9, 2022
Code Specialist 27 Oct 16, 2022
The lightning-fast ASGI server. ?

The lightning-fast ASGI server. Documentation: https://www.uvicorn.org Community: https://discuss.encode.io/c/uvicorn Requirements: Python 3.6+ (For P

Encode 6k Jan 3, 2023
The lightning-fast ASGI server. 🦄

The lightning-fast ASGI server. Documentation: https://www.uvicorn.org Community: https://discuss.encode.io/c/uvicorn Requirements: Python 3.6+ (For P

Encode 6k Jan 3, 2023