# PrometheusRock
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.


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.
   
   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).
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)

