1616from starlette .requests import Request
1717from starlette .responses import Response
1818from starlette .routing import Match , Mount
19+ from starlette .types import Scope
1920
21+ TIMER_ATTRIBUTE = "__fastapi_utils_timer__"
22+
23+
24+ def add_timing_middleware (
25+ app : FastAPI , record : Optional [Callable [[str ], None ]] = None , prefix : str = "" , exclude : Optional [str ] = None
26+ ) -> None :
27+ """
28+ Adds a middleware to the provided `app` that records timing metrics using the provided `record` callable.
29+
30+ Typically `record` would be something like `logger.info` for a `logging.Logger` instance.
31+
32+ The provided `prefix` is used when generating route names.
33+
34+ If `exclude` is provided, timings for any routes containing `exclude`
35+ as an exact substring of the generated metric name will not be logged.
36+ This provides an easy way to disable logging for routes
37+
38+ The `exclude` will probably be replaced by a regex match at some point in the future. (PR welcome!)
39+ """
40+ metric_namer = _MetricNamer (prefix = prefix , app = app )
41+
42+ @app .middleware ("http" )
43+ async def timing_middleware (request : Request , call_next : RequestResponseEndpoint ) -> Response :
44+ metric_name = metric_namer (request .scope )
45+ with _TimingStats (metric_name , record = record , exclude = exclude ) as timer :
46+ setattr (request .state , TIMER_ATTRIBUTE , timer )
47+ response = await call_next (request )
48+ return response
49+
50+
51+ def record_timing (request : Request , note : Optional [str ] = None ) -> None :
52+ """
53+ Call this function at any point that you want to display elapsed time during the handling of a single request
54+
55+ This can help profile which piece of a request is causing a performance bottleneck.
56+
57+ Note that for this function to succeed, the request should have been generated by a FastAPI app
58+ that has had timing middleware added using the `fastapi_utils.timing.add_timing_middleware` function.
59+ """
60+ timer = getattr (request .state , TIMER_ATTRIBUTE , None )
61+ if timer is not None :
62+ assert isinstance (timer , _TimingStats )
63+ timer .emit (note )
64+ else :
65+ raise ValueError ("No timer present on request" )
66+
67+
68+ class _TimingStats :
69+ """
70+ This class tracks and records endpoint timing data.
71+
72+ Should be used as a context manager; on exit, timing stats will be emitted.
73+
74+ name:
75+ The name to include with the recorded timing data
76+ record:
77+ The callable to call on generated messages. Defaults to `print`, but typically
78+ something like `logger.info` for a `logging.Logger` instance would be preferable.
79+ exclude:
80+ An optional string; if it is not None and occurs inside `name`, no stats will be emitted
81+ """
2082
21- class TimingStats :
2283 def __init__ (
2384 self , name : Optional [str ] = None , record : Callable [[str ], None ] = None , exclude : Optional [str ] = None
2485 ) -> None :
@@ -31,16 +92,16 @@ def __init__(
3192 self .end_time : float = 0
3293 self .silent : bool = False
3394
34- if self .name and exclude and (exclude in self .name ):
95+ if self .name is not None and exclude is not None and (exclude in self .name ):
3596 self .silent = True
3697
3798 def start (self ) -> None :
3899 self .start_time = time .time ()
39- self .start_cpu_time = get_cpu_time ()
100+ self .start_cpu_time = _get_cpu_time ()
40101
41102 def take_split (self ) -> None :
42103 self .end_time = time .time ()
43- self .end_cpu_time = get_cpu_time ()
104+ self .end_cpu_time = _get_cpu_time ()
44105
45106 @property
46107 def time (self ) -> float :
@@ -50,14 +111,17 @@ def time(self) -> float:
50111 def cpu_time (self ) -> float :
51112 return self .end_cpu_time - self .start_cpu_time
52113
53- def __enter__ (self ) -> "TimingStats " :
114+ def __enter__ (self ) -> "_TimingStats " :
54115 self .start ()
55116 return self
56117
57118 def __exit__ (self , exc_type : Any , exc_value : Any , traceback : Any ) -> None :
58119 self .emit ()
59120
60121 def emit (self , note : Optional [str ] = None ) -> None :
122+ """
123+ Emit timing information, optionally including a specified note
124+ """
61125 if not self .silent :
62126 self .take_split ()
63127 cpu_ms = 1000 * self .cpu_time
@@ -68,14 +132,35 @@ def emit(self, note: Optional[str] = None) -> None:
68132 self .record (message )
69133
70134
71- class MetricNamer :
135+ class _MetricNamer :
136+ """
137+ This class generates the route "name" used when logging timing records.
138+
139+ If the route has `endpoint` and `name` attributes, the endpoint's module and route's name will be used
140+ (along with an optional prefix that can be used, e.g., to distinguish between multiple mounted ASGI apps).
141+
142+ By default, in FastAPI the route name is the `__name__` of the route's function (or type if it is a callable class
143+ instance).
144+
145+ For example, with prefix == "custom", a function defined in the module `app.crud` with name `read_item`
146+ would get name `custom.app.crud.read_item`. If the empty string were used as the prefix, the result would be
147+ just "app.crud.read_item".
148+
149+ For starlette.routing.Mount instances, the name of the type of `route.app` is used in a slightly different format.
150+
151+ For other routes missing either an endpoint or name, the raw route path is included in the generated name.
152+ """
153+
72154 def __init__ (self , prefix : str , app : FastAPI ):
73155 if prefix :
74156 prefix += "."
75157 self .prefix = prefix
76158 self .app = app
77159
78- def __call__ (self , scope : Any ) -> str :
160+ def __call__ (self , scope : Scope ) -> str :
161+ """
162+ Generates the actual name to use when logging timing metrics for a specified ASGI Scope
163+ """
79164 route = None
80165 for r in self .app .router .routes :
81166 if r .matches (scope )[0 ] == Match .FULL :
@@ -90,36 +175,10 @@ def __call__(self, scope: Any) -> str:
90175 return name
91176
92177
93- def get_cpu_time () -> float :
94- # taken from timing-asgi
178+ def _get_cpu_time () -> float :
179+ """
180+ Generates the cpu time to report. Adds the user and system time, following the implementation from timing-asgi
181+ """
95182 resources = resource .getrusage (resource .RUSAGE_SELF )
96183 # add up user time (ru_utime) and system time (ru_stime)
97184 return resources [0 ] + resources [1 ]
98-
99-
100- def add_timing_middleware (
101- app : FastAPI , record : Callable [[str ], None ] = None , prefix : str = "" , exclude : Optional [str ] = None
102- ) -> None :
103- """
104- Don't print timings if exclude occurs as an exact substring of the generated metric name
105- """
106- metric_namer = MetricNamer (prefix = prefix , app = app )
107-
108- @app .middleware ("http" )
109- async def timing_middleware (request : Request , call_next : RequestResponseEndpoint ) -> Response :
110- metric_name = metric_namer (request .scope )
111- with TimingStats (metric_name , record = record , exclude = exclude ) as timer :
112- request .state .timer = timer
113- response = await call_next (request )
114- return response
115-
116-
117- def record_timing (request : Request , note : Optional [str ] = None ) -> None :
118- """
119- Call this function anywhere you want to display performance information while handling a single request
120- """
121- if hasattr (request .state , "timer" ):
122- assert isinstance (request .state .timer , TimingStats )
123- request .state .timer .emit (note )
124- else :
125- print ("TIMING ERROR: No timer present on request" )
0 commit comments