雑記

参考文献: client_python/metrics.py at master · prometheus/client_python · GitHub

あなたは弁当販売を始めることにしました。A か B の type を指定してリクエストしてもらい、それに応じてA弁当(バターチキンカレー)かB弁当(ローストビーフ)を渡したいと思います。以下のようにすれば、localhost:8080/bentou?type=A にアクセスするとバターチキンカレーが返却されます。

import asyncio
from aiohttp import web

class BentouHandler:
    async def handle(self, request):
        params = {k: v for k, v in request.query.items() if v}
        if 'type' not in params:
            raise ValueError('type がありません')
        type_ = params['type']
        bentou = None
        if type_ == 'A':
            await asyncio.sleep(2)
            bentou = {'主食': 'ナン', 'おかず': 'バターチキンカレー'}
        elif type_ == 'B':
            await asyncio.sleep(1)
            bentou = {'主食': 'ごはん', 'おかず': 'ローストビーフ'}
        else:
            raise ValueError('type が不正です')
        return web.json_response(bentou, status=200)

handler = BentouHandler()
app = web.Application()
app.add_routes([web.get(f'/bentou', handler.handle)])

if __name__ == '__main__':
    web.run_app(app, port=8080)

ここであなたは、これまでに弁当が何個売れたか、実際に弁当を提供するのにかかった時間のヒストグラムはどうなっているかを知りたいと思いました。以下のようにすると localhost:8080/metrics からこれまでの総リクエスト数とレスポンス時間の累積分布が確認できます。

import asyncio
from aiohttp import web
from prometheus_client import Counter, Histogram, generate_latest

class BentouHandler:
    async def handle(self, request):
        params = {k: v for k, v in request.query.items() if v}
        if 'type' not in params:
            raise ValueError('type がありません')
        type_ = params['type']
        bentou = None
        if type_ == 'A':
            await asyncio.sleep(2)
            bentou = {'主食': 'ナン', 'おかず': 'バターチキンカレー'}
        elif type_ == 'B':
            await asyncio.sleep(1)
            bentou = {'主食': 'ごはん', 'おかず': 'ローストビーフ'}
        else:
            raise ValueError('type が不正です')
        return web.json_response(bentou, status=200)

class BentouMetrics:
    def __init__(self):
        # 必要なカウンタを用意
        self.get_req_counter = Counter('get_request_count', 'リクエスト数')
        self.resp_time_hist = Histogram('resp_time_hist', 'レスポンスタイム', buckets=[0.5, 1.5, 2.5])

    @web.middleware
    async def wrapper(self, request, handler):  # ハンドラをラップして各種メトリクスを計測する
        # 目的のエンドポイント以外へのリクエストは計測対象外
        if request.path not in ['/bentou']:
            return await handler(request)

        # 各種メトリクスを計測する
        self.get_req_counter.inc()  # リクエスト数をインクリメント
        with self.resp_time_hist.time():  # レスポンス時間を計測
            response = await handler(request)
        return response

    async def exposer(self, request):  # 現在のメトリクスを返却する
        return web.Response(body=generate_latest(), content_type='text/plain')

    @classmethod
    def setup(cls, app):
        metrics = cls()
        app.middlewares.append(metrics.wrapper)
        app.router.add_get('/metrics', metrics.exposer)

handler = BentouHandler()
app = web.Application()
BentouMetrics.setup(app)
app.add_routes([web.get(f'/bentou', handler.handle)])
...
# HELP get_request_count_total リクエスト数
# TYPE get_request_count_total counter
get_request_count_total 2.0
# HELP resp_time_hist レスポンスタイム
# TYPE resp_time_hist histogram
resp_time_hist_bucket{le="0.5"} 0.0
resp_time_hist_bucket{le="1.5"} 1.0
resp_time_hist_bucket{le="2.5"} 2.0
resp_time_hist_bucket{le="+Inf"} 2.0
...

しかし上記の方法だと正常に弁当を返せなかったときのレスポンス時間もヒストグラムに含まれてしまいます。正常に弁当を返せなかったときは、回数は記録するがレスポンス時間は計測しないようにするには以下のようにすると思います。

import time

class BentouMetrics:
    def __init__(self):
        # 必要なカウンタを用意
        self.get_req_counter = Counter('get_request_count', '正常に返せたリクエスト数')
        self.get_err_req_counter = Counter('get_error_request_count', '正常に返せなかったリクエスト数')
        self.resp_time_hist = Histogram('resp_time_hist', '正常レスポンスタイム', buckets=[0.5, 1.5, 2.5])

    @web.middleware
    async def wrapper(self, request, handler):  # ハンドラをラップして各種メトリクスを計測する
        # 目的のエンドポイント以外へのリクエストは計測対象外
        if request.path not in ['/bentou']:
            return await handler(request)

        # 各種メトリクスを計測する
        time_0 = time.perf_counter()
        try:
            response = await handler(request)
            self.get_req_counter.inc()  # リクエスト数をインクリメント
            self.resp_time_hist.observe(time.perf_counter() - time_0)  # レスポンス時間を記録
        except Exception as e:
            self.get_err_req_counter.inc()  # 正常に返せなかったリクエスト数をインクリメント
            raise e
        return response