33
44import logging
55from typing import TYPE_CHECKING , Callable , Dict , List , Optional , Union
6- from prometheus_client import CONTENT_TYPE_LATEST , REGISTRY , Counter , Histogram , generate_latest
6+
7+ from prometheus_client import CONTENT_TYPE_LATEST , Counter , Histogram , generate_latest , CollectorRegistry
78from quart import Response , g , request
89from quart .exceptions import HTTPException , HTTPStatusException
9- from .utils import now_utc
10+
11+ from utils import now_utc
1012
1113if TYPE_CHECKING :
1214 from quart import Quart
@@ -66,14 +68,15 @@ def __init__(self, app: Optional[Quart] = None, metrics_endpoint: str = "root"):
6668 self ._collectors : Dict [str , MetricType ] = {}
6769 self ._custom_labeler : Optional [Callable [["LocalProxy" ], Dict [str , str ]]] = None
6870 self ._custom_label_names : List [str ] = []
71+ self ._registry = CollectorRegistry ()
6972 self ._register_collectors ()
7073 if app :
7174 self .init_app (app , metrics_endpoint )
7275
7376 def init_app (self , app : Quart , metrics_endpoint : str ):
7477 """Register an application."""
7578
76- def start_request ():
79+ def prometheus_before_request_callback ():
7780 if request .path == "/metrics" :
7881 return
7982 g .start = now_utc () # type: ignore # This is a valid use of Quart's global object
@@ -82,7 +85,7 @@ def start_request():
8285 request .content_length or 0
8386 )
8487
85- def end_request (response ):
88+ def prometheus_after_request_callback (response : Response ):
8689 if request .path == "/metrics" :
8790 return response
8891 if not hasattr (g , "start" ):
@@ -105,17 +108,27 @@ def end_request(response):
105108 ).inc ()
106109 return response
107110
108- def abort_with_error (exc : Union [HTTPException , Exception ]) -> Response :
111+ def prometheus_error_handler (exc : Union [HTTPException , Exception ]) -> Response :
109112 if isinstance (exc , HTTPException ):
110113 response = exc .get_response ()
111114 else :
112115 response = Response ("" , 500 )
113- return end_request (response )
116+ return prometheus_after_request_callback (response )
114117
115118 self .app = app
116- app .before_request (start_request )
117- app .after_request (end_request )
118- app .register_error_handler (HTTPStatusException , abort_with_error )
119+ app .before_request (
120+ prometheus_before_request_callback ,
121+ prometheus_before_request_callback .__name__ ,
122+ )
123+ app .after_request (
124+ prometheus_after_request_callback ,
125+ prometheus_after_request_callback .__name__ ,
126+ )
127+ app .register_error_handler (
128+ HTTPStatusException ,
129+ prometheus_error_handler , # type:ignore
130+ prometheus_error_handler .__name__ ,
131+ )
119132 app .add_url_rule ("/metrics" , metrics_endpoint , view_func = self .render )
120133
121134 def _register_collectors (self ):
@@ -127,46 +140,57 @@ def _register_collectors(self):
127140 "http_requests" ,
128141 "Total number of requests" ,
129142 ["method" , "path" , "status" , * self ._custom_label_names ],
143+ registry = self ._registry ,
130144 ),
131145 Counter (
132146 "http_requests_errors" ,
133147 "Total number of error requests" ,
134148 ["method" , "path" , "status" , * self ._custom_label_names ],
149+ registry = self ._registry ,
135150 ),
136151 Histogram (
137152 "http_request_duration_seconds" ,
138153 "The amount of time spent handling requests" ,
139154 ["path" , * self ._custom_label_names ],
155+ registry = self ._registry ,
140156 ),
141157 Histogram (
142158 "http_request_size_bytes" ,
143159 "The size of requests" ,
144160 ["path" , * self ._custom_label_names ],
145161 buckets = REQUEST_BUCKETS ,
162+ registry = self ._registry ,
146163 ),
147164 Histogram (
148165 "http_response_size_bytes" ,
149166 "The size of responses" ,
150167 ["path" , * self ._custom_label_names ],
151168 buckets = RESPONSE_BUCKETS ,
169+ registry = self ._registry ,
152170 ),
153171 )
154172 }
155173
174+ def _reset_metrics (self ):
175+ """Unregister all collectors from the registry."""
176+ for _ , collector in self ._collectors .items ():
177+ self ._registry .unregister (collector )
178+
156179 def custom_route_labeler (
157180 self , labeler : Callable [["LocalProxy" ], Dict [str , str ]], label_names : List [str ]
158181 ) -> None :
159182 """Add a handler for providing additional labels for a route.
160183
161184 This will reset all metrics. Conventionally it's called when the extension is first registered
162185
163- :param labeler: The handler function to invoke. It must return a dict of key-value labels.
186+ :param labeler: The handler function to invoke. It takes a Quart LocalProxy representing
187+ the request, from which values for the custom label can be pulled. It must return
188+ a dict of key-value labels.
164189 :param label_names: The possible label names emitted by the labeler.
165190 """
166191 self ._custom_labeler = labeler
167192 self ._custom_label_names = label_names
168- for _ , collector in self ._collectors .items ():
169- REGISTRY .unregister (collector )
193+ self ._reset_metrics ()
170194 self ._register_collectors ()
171195
172196 def get (self , name : str ) -> MetricType :
@@ -181,4 +205,3 @@ def get(self, name: str) -> MetricType:
181205 def render ():
182206 """Render the stats."""
183207 return Response (generate_latest (), mimetype = CONTENT_TYPE_LATEST )
184-
0 commit comments