aurweb/aurweb/prometheus.py
Kevin Morris 0da11f068b
fix(fastapi): check for prometheus info.response
When this is unchecked, exceptions cause the resulting stack
trace to be oblivious to the original exception thrown.

This commit changes that behavior so that metrics are created
only when info.response exists.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-11 16:24:10 -08:00

103 lines
3.7 KiB
Python

from typing import Any, Callable, Dict, List, Optional
from prometheus_client import Counter
from prometheus_fastapi_instrumentator import Instrumentator
from prometheus_fastapi_instrumentator.metrics import Info
from starlette.routing import Match, Route
from aurweb import logging
logger = logging.get_logger(__name__)
_instrumentator = Instrumentator()
def instrumentator():
return _instrumentator
# Taken from https://github.com/stephenhillier/starlette_exporter
# Their license is included in LICENSES/starlette_exporter.
# The code has been modified to remove child route checks
# (since we don't have any) and to stay within an 80-width limit.
def get_matching_route_path(scope: Dict[Any, Any], routes: List[Route],
route_name: Optional[str] = None) -> str:
"""
Find a matching route and return its original path string
Will attempt to enter mounted routes and subrouters.
Credit to https://github.com/elastic/apm-agent-python
"""
for route in routes:
match, child_scope = route.matches(scope)
if match == Match.FULL:
route_name = route.path
'''
# This path exists in the original function's code, but we
# don't need it (currently), so it's been removed to avoid
# useless test coverage.
child_scope = {**scope, **child_scope}
if isinstance(route, Mount) and route.routes:
child_route_name = get_matching_route_path(child_scope,
route.routes,
route_name)
if child_route_name is None:
route_name = None
else:
route_name += child_route_name
'''
return route_name
elif match == Match.PARTIAL and route_name is None:
route_name = route.path
def http_requests_total() -> Callable[[Info], None]:
metric = Counter("http_requests_total",
"Number of HTTP requests.",
labelnames=("method", "path", "status"))
def instrumentation(info: Info) -> None:
scope = info.request.scope
# Taken from https://github.com/stephenhillier/starlette_exporter
# Their license is included at LICENSES/starlette_exporter.
# The code has been slightly modified: we no longer catch
# exceptions; we expect this collector to always succeed.
# Failures in this collector shall cause test failures.
if not (scope.get("endpoint", None) and scope.get("router", None)):
return None
base_scope = {
"type": scope.get("type"),
"path": scope.get("root_path", "") + scope.get("path"),
"path_params": scope.get("path_params", {}),
"method": scope.get("method")
}
method = scope.get("method")
path = get_matching_route_path(base_scope, scope.get("router").routes)
if info.response:
status = str(int(info.response.status_code))[:1] + "xx"
metric.labels(method=method, path=path, status=status).inc()
return instrumentation
def http_api_requests_total() -> Callable[[Info], None]:
metric = Counter(
"http_api_requests",
"Number of times an RPC API type has been requested.",
labelnames=("type", "status"))
def instrumentation(info: Info) -> None:
if info.request.url.path.rstrip("/") == "/rpc":
type = info.request.query_params.get("type", "None")
if info.response:
status = str(info.response.status_code)[:1] + "xx"
metric.labels(type=type, status=status).inc()
return instrumentation