Prometheus middleware for Starlette and FastAPI
This middleware collects couple of basic metrics and allow you to add your own ones.
Basic metrics:
- Counter: requests_total
- Histogram: request_processing_time
Basic labels for them:
- method
- path
- status_code
User-Agent
andHost
headers- application name
Example:
request_processing_time_sum{app_name="test_app",headers="{'host': '127.0.0.1:8020', 'user-agent': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:81.0) Gecko/20100101 Firefox/81.0'}",path="/test",status_code="200"} 0.00036406517028808594
Metrics include labels for the HTTP method, the path, and the response status code.
Set for path /metrics
handler metrics_route
and your metrics will be exposed on that url for Prometheus further use.
If you don't want nothing extra, this is for you. Grab the code and run to paste it!
For starlette and FastAPI init part pretty similar.
-
First:
pip install prometheusrock
-
Second:
Choose your fighter! If you're using starlette:
from starlette.applications import Starlette
And if you're using FastAPI:
from fastapi import FastAPI
Moving further:
from prometheusrock import PrometheusMiddleware, metrics_route app = # Starlette() or FastAPI() app.add_middleware(PrometheusMiddleware) app.add_route("/metrics", metrics_route) ...
And that's it! Now go on
/metrics
and see your logs!
If you want to configure basic metrics let me show you how!
When you declare middleware, you can pass following args:
app_name
- the name you want to show in metrics as the name of your app. Default - "ASGIApp",additional_headers
- if you want to track additional headers (aside of default ones -user-agent
andhost
) you can passlist
(that's important!) with names of that headers. They all cast to lowercase, so casing doesn't matters.remove_labels
- by default basic metrics labels are following:method
,path
,status_code
,headers
,app_name
. If you don't wanna some of them - passlist
with their names here. And their gone!skip_paths
- sometimes you don't wanna log some of the endpoint. (Fore example you don't wanna log accesses to/metrics
in your metrics). If you want to exclude this paths from metric - pass herelist
with their urls. By default this middleware ignores/metrics
route, so if you initially moved your metric route to some other url - pass it here. If you want to log all routes (even the default/metrics
- pass an empty list!)disable_default_counter
- if you want to disable default Counter metric - passTrue
value to this optional param.disable_default_histogram
- if you want to disable default Histogram metric - passTrue
value to this optional param.custom_base_labels
- if you want change default labels to yours - pass them here. REWRITES DEFAULT LABLES. Argsremove_labels
WILL BE IGNORED.
example -['path','method']
- and you have metric, that contains onlypath
andmethod
labels.custom_base_headers
- if you want change default headers to yours - pass them here. REWRITES DEFAULT HEADERS. Argsadditional_headers
WILL BE IGNORED. If you usecustom_base_labels
, don't forget to passheaders
in it, otherwisecustom_base_headers
will have no effect.
example -['content-type','x-api-client']
- and now you write only these two headers.aggregate_paths
- if you have endpoints like/item/{id}
, then, by default, your logs will quickly overflow, showing you huge amount of numbers, when, in fact, endpoint is one. So pass here list of endpoints path to aggregate by. example -['/item/']
But a picture is worth a thousand words, right? Let's see some code!
For example, we want our middleware to have a following settings:
we want a name this_is_my_app
, we want to track header accept-encoding
, we don't wanna label path
(if you have one endpoint for example),
and we don't want url /_healthcheck
to be tracked.
app.add_middleware(
PrometheusMiddleware,
app_name='this_is_my_app',
additional_headers=['accept-encoding'],
remove_labels=['path'],
skip_paths=['/_healthcheck']
)
And after that, our metric will look something like that:
requests_total{app_name="this_is_my_app",headers="{'host': '127.0.0.1:8000', 'user-agent': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:81.0) Gecko/20100101 Firefox/81.0', 'accept-encoding': 'gzip, deflate'}",method="GET",status_code="200"} 1.0
And the star of the evening - custom metrics! So, lets suppose you want to check how many are rows in your Database after each request. Let's explore this:
First, we do all the same things - we initiate the app, we add PrometheusMiddleware. And the next steps are:
-
We must decide what type of metric we want - choose one from here. Basically, you will need pass one of the types -
info, gauge, counter, histogram, summary, enum
. -
We declare the function that will act like our metric logic:
# async here isn't necessary, you can use ordinary function async def query(middleware_proxy): res = await db.execute_query( "SELECT COUNT(*) as count from MyTable" ) middleware_proxy.metric.labels(**res)
Function MUST accept this argument. Obviously you can name it however you want, as long is it still there. If you want to know what's inside -
from prometheusrock import Metric
. I strongly recommend to pass it as typehinting:from prometheusrock import Metric ... async def query(middleware_proxy: Metric):
Metric have 3 attributes:
- metric - instance of
prometheus_client
metric object. - metric_type - string with type.
- spent_time - time, that was spent on request. You may need it if you, for example, implementing Histogram metric.
- request - request object from app.
And now IMPORTANT remark - you must correctly invoke metric! So if you, for example, chose
Counter
metric, in your custom function you must domiddleware_proxy.metric.labels(**res).inc()
, or if you chose Histogram -middleware_proxy.metric.labels(**res).observe(middleware_proxy.spent_time)
and so on, according to this docs. Value that you're passing there -res
(or however you called it) must be a sequence of the parameters, that you set as lables for your metric. For example, if your metric have labelscount
andid
,res
must be a dictionary{"count": count, "id": id}
or list with right positioning -[count, id]
. - metric - instance of
-
And finally we tell our middleware about our custom metric:
from prometheusrock import AddMetric, PrometheusMiddleware ... app.add_middleware(PrometheusMiddleware) ... # async here isn't necessary, you can use ordinary function async def query(middleware_proxy): res = await db.execute_query( "SELECT COUNT(*) as count from MyTable" ) middleware_proxy.metric.labels(**res) AddMetric( function=query, metric_name='my_precious', metric_type='info', labels=['row_count'] )
AddMetric accept following params:
- function - function that will work as your metric logic
- metric_name - unique metric name, must be ONE-WORDED (e.g. unique_metric_name). Default - "user_metric".
- metric_description- description of your metric. Default- "description of user metric".
- labels - list of lables that you want your metric to contain. Default - ["info"].
- metric_type - one of
prometheus_client
metric types - described in paragraph 1.
Dependencies: Starlette, client_python
Additional links: FastAPI