fixed prometheus statistics to correctly use labels

This commit is contained in:
Jenny Danzmayr 2024-03-30 20:04:11 +01:00
parent 9da37855c0
commit d1ba39df1c
4 changed files with 57 additions and 3 deletions

View file

@ -81,6 +81,8 @@ def api_etag(permissions=True, etag_func=AccessPermission.etag_func, base_mapdat
def api_stats(stat_name): def api_stats(stat_name):
from c3nav.mapdata.metrics import APIStatsCollector
APIStatsCollector.add_stat(stat_name, ['by', 'query'])
def wrapper(func): def wrapper(func):
@wraps(func) @wraps(func)
def wrapped_func(request, *args, **kwargs): def wrapped_func(request, *args, **kwargs):

View file

@ -1,4 +1,6 @@
import re import re
from typing import Optional, Sequence
from django.conf import settings from django.conf import settings
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.core.cache import cache from django.core.cache import cache
@ -18,15 +20,54 @@ if settings.METRCIS:
reports_open.set_function(lambda: Report.objects.filter(open=True).count()), reports_open.set_function(lambda: Report.objects.filter(open=True).count()),
class APIStatsCollector(Collector): class APIStatsCollector(Collector):
name_registry: dict[str, None | Sequence[str]] = dict()
def collect(self): def collect(self):
metrics: dict[str, CounterMetricFamily] = dict()
if settings.CACHES['default']['BACKEND'] == 'django.core.cache.backends.redis.RedisCache': if settings.CACHES['default']['BACKEND'] == 'django.core.cache.backends.redis.RedisCache':
client = cache._cache.get_client() client = cache._cache.get_client()
for key in client.keys(f"*{settings.CACHES['default'].get('KEY_PREFIX', '')}apistats__*"): for key in client.keys(f"*{settings.CACHES['default'].get('KEY_PREFIX', '')}apistats__*"):
key = key.decode('utf-8').split(':', 2)[2] key: str = key.decode('utf-8').split(':', 2)[2]
key = re.sub(r'[^a-zA-Z0-9_]', '_', key) value = cache.get(key)
yield CounterMetricFamily(f'c3nav_{key}', key, value=cache.get(key)) key = key[10:] # trim apistats__ from the beginning
# some routing stats don't use double underscores to separate fields, workaround for now
if key.startswith('route_tuple_'):
key = re.sub(r'^route_tuple_(.*)_(.*)$', r'route_tuple__\1__\2', key)
if key.startswith('route_origin_') or key.startswith('route_destination_'):
key = re.sub(r'^route_(origin|destination)_(.*)$', r'route_\1__\2', key)
name, *labels = key.split('__')
try:
label_names = self.name_registry[name]
except KeyError:
continue
if label_names is None:
label_names = list()
if len(label_names) != len(labels):
raise ValueError('configured labels and number of extracted labels doesn\'t match.')
try:
counter = metrics[name]
except KeyError:
counter = metrics[name] = CounterMetricFamily(f'c3nav_{name}', f'c3nav_{name}',
labels=label_names)
counter.add_metric(labels, value)
return metrics.values()
def describe(self): def describe(self):
return list() return list()
@classmethod
def add_stat(cls, name:str, label_names: Optional[str | Sequence[str]] = None):
if isinstance(label_names, str):
label_names = [label_names]
if name in cls.name_registry and label_names != cls.name_registry[name]:
raise KeyError(f'{name} already exists')
cls.name_registry[name] = label_names
REGISTRY.register(APIStatsCollector()) REGISTRY.register(APIStatsCollector())

View file

@ -6,6 +6,7 @@ from ninja import Router as APIRouter
from c3nav.api.auth import auth_responses from c3nav.api.auth import auth_responses
from c3nav.api.schema import BaseSchema from c3nav.api.schema import BaseSchema
from c3nav.mapdata.metrics import APIStatsCollector
from c3nav.mapdata.models.access import AccessPermission from c3nav.mapdata.models.access import AccessPermission
from c3nav.mapdata.schemas.models import CustomLocationSchema from c3nav.mapdata.schemas.models import CustomLocationSchema
from c3nav.mapdata.utils.cache.stats import increment_cache_key from c3nav.mapdata.utils.cache.stats import increment_cache_key
@ -51,6 +52,9 @@ def get_position(request, parameters: LocateRequestSchema):
} }
APIStatsCollector.add_stat('locate', 'location')
@positioning_api_router.get('/locate-test/', summary="debug position", @positioning_api_router.get('/locate-test/', summary="debug position",
description="outputs a location for debugging purposes", description="outputs a location for debugging purposes",
response={200: PositioningResult, **auth_responses}) response={200: PositioningResult, **auth_responses})

View file

@ -14,6 +14,7 @@ from c3nav.api.exceptions import APIRequestValidationFailed
from c3nav.api.schema import BaseSchema from c3nav.api.schema import BaseSchema
from c3nav.api.utils import NonEmptyStr from c3nav.api.utils import NonEmptyStr
from c3nav.mapdata.api.base import api_stats_clean_location_value from c3nav.mapdata.api.base import api_stats_clean_location_value
from c3nav.mapdata.metrics import APIStatsCollector
from c3nav.mapdata.models.access import AccessPermission from c3nav.mapdata.models.access import AccessPermission
from c3nav.mapdata.models.locations import Position from c3nav.mapdata.models.locations import Position
from c3nav.mapdata.schemas.model_base import AnyLocationID, Coordinates3D from c3nav.mapdata.schemas.model_base import AnyLocationID, Coordinates3D
@ -251,6 +252,12 @@ def get_route(request, parameters: RouteParametersSchema):
) )
APIStatsCollector.add_stat('route')
APIStatsCollector.add_stat('route_tuple', ['origin', 'destination'])
APIStatsCollector.add_stat('route_origin', ['origin'])
APIStatsCollector.add_stat('route_destination', ['destination'])
def _new_serialize_route_options(options): def _new_serialize_route_options(options):
# todo: RouteOptions should obviously be modernized # todo: RouteOptions should obviously be modernized
main_options = {} main_options = {}