Prometheus & Flask: meer dan één metrics-endpoint

De titel is misschien een beetje vreemd, maar het wordt al snel duidelijker. Ik werk aan een applicatie die op meerdere plaatsen (endpoints) metrics wil beschikbaar stellen via het Prometheus-formaat. In normale situaties heb je maar één endpoint en dan kan je gewoon de Python-library “as is” gebruiken. Maar zo niet in mijn geval. Ik wil veel meer endpoints, en dat kan. Alleen is het niet meteen duidelijk hoe je dat moet doen, en om iedereen mijn zoektocht te besparen heb ik het ter leering ende vermaek opgeschreven.

Ieder endpoint hangt vast aan een REGISTRY en je registreert metrics (via een collector) in die registry. Met generate_latest kan je dan de standaardregistry omzetten in het tekstformaat dat Prometheus begrijpt. Tot dusver duidelijk en goed beschreven.

Maar, meerdere registries zijn mogelijk, en je kan dan voor iedere endpoint (een endpoint is uiteindelijk ook gewoon een URL) een aparte registry maken en weergeven. Als jouw applicatie een soort gateway is waar data uit allerlei onderliggende systemen samenkomt kan je zo vermijden om één /metrics-pagina te genereren met een paar tienduizend lijnen op.

Beginnen doen we door een registry aan te maken.

from prometheus_client import generate_latest, CollectorRegistry, GaugeMetricFamily


custom_registry = CollectorRegistry()

In een volgende stap maken we onze eigen collector. Behalve dat een collector een methode collect moet hebben die een metric (of familie zoals hier) yield zijn er geen verdere eisen. Je kan dus één van jouw bibliotheken/klasses een collector maken om zo bepaalde metrics bij de bron al uit te spuwen.


class MyDataClass(object):

    db = []

    def fetch_data(self):
        """
        A sample method that can generate some data.
        """
        self.db = []
    
    def collect(self):
        gauge = GaugeMetricFamily('data_size', 'Size of the data returned by this class.', labels=['instance'])
        gauge.add_metric(['instance1'], len(self.db))
        yield gauge

We hebben nu een collector, nu moeten we die nog een aan een registry hangen. We gebruiken hiervoor niet de standaard, maar degene die we zonet hebben aangemaakt. Let erop dat je een instance (met ()) registreert en niet de klasse zelf.


custom_registry.register(MyDateClass())

De data is er nu wel, maar je kan nog helemaal niets zien. We lossen dat op door een route te maken (ik gebruik Flask, dus dat is @application.route('/my/metrics')) waar we dan met generate_latest de metrics weergeven in het Prometheusformaat.

from prometheus_client import generate_latest
from flask import Response, Flask

application = Flask(__name__)

@application.route('/my/metrics/')
def metrics():
    return Response(generate_latest(CollectorRegistry), mimetype='text/plain; version=0.0.4; charset=utf-8')

Let er hier op dat je hier de klasse en niet de instance meegeeft.

Het is een beetje een omweg, maar het is wel een stuk eenvoudiger om structuur en orde in je applicatie te krijgen. Misschien niet echt nuttig voor een klein programma, maar als het gaat om een mammoetproject met héél veel verschillende rollen is het absoluut onmisbaar.