Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/kozhushman/prometheusrock

Prometheus middleware for Starlette and FastAPI
https://github.com/kozhushman/prometheusrock

custom-metrics fastapi metrics middleware prometheus prometheus-middleware starlette

Last synced: 4 months ago
JSON representation

Prometheus middleware for Starlette and FastAPI

Awesome Lists containing this project

README

        

# PrometheusRock
![Python package](https://github.com/kozhushman/prometheusrock/workflows/Python%20package/badge.svg?branch=main)
![CodeQL](https://github.com/kozhushman/prometheusrock/workflows/CodeQL/badge.svg?branch=main)

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` and `Host` headers
* application name

Example:
```sh
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.

## Usage

### 1. I don't want anything custom, just give me the basics!
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.

1. First:
```
pip install prometheusrock
```
2. Second:

Choose your fighter!
If you're using starlette:
```python
from starlette.applications import Starlette
```
And if you're using FastAPI:
```python
from fastapi import FastAPI
```
Moving further:
```python
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!

### 2. Custom you say? Let me see...but just a little!
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` and `host`)
you can pass `list` (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 - pass `list` 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 here `list` 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 - pass `True` value to this optional param.
* `disable_default_histogram` - if you want to disable default Histogram metric - pass `True` value to this optional param.
* `custom_base_labels` - if you want change default labels to yours - pass them here.
**REWRITES DEFAULT LABLES**. Args `remove_labels` **WILL BE IGNORED**.
example - `['path','method']` - and you have metric, that contains only `path` and `method` labels.
* `custom_base_headers` - if you want change default headers to yours - pass them here.
**REWRITES DEFAULT HEADERS**. Args `additional_headers` **WILL BE IGNORED**.
If you use `custom_base_labels`, don't forget to pass `headers` in it,
otherwise `custom_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.
```python
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:
```sh
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
```

## Let's go deeper! Add your own custom metric!

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:
1. We must decide what type of metric we want - [choose one from here](https://github.com/prometheus/client_python). Basically, you will need pass one of the types - `info, gauge, counter, histogram, summary, enum`.
2. We declare the function that will act like our metric logic:
```python
# 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:
```python
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 do `middleware_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](https://github.com/prometheus/client_python).
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 labels `count` and `id`, `res` must be
a dictionary `{"count": count, "id": id}` or list with right positioning - `[count, id]`.

3. And finally we tell our middleware about our custom metric:
```python
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.

## Links and dependencies

Dependencies:
[Starlette](https://github.com/encode/starlette),
[client_python](https://github.com/prometheus/client_python)

Additional links:
[FastAPI](https://github.com/tiangolo/fastapi)