Add frontend performance metrics pipeline to Prometheus
Collect browser-side worker round-trips, computation times, main-thread operations, and feature counts, batch them client-side, and expose as Prometheus histograms via a new POST /api/perf endpoint.
This commit is contained in:
parent
c24c3a545c
commit
d90fa38776
10 changed files with 188 additions and 5 deletions
|
|
@ -9,6 +9,7 @@ from api.auth import get_current_user
|
|||
from api.config import DEV_TIER_ORIGINS, PROD_TIER_ORIGINS, APP_ENV
|
||||
from api.decision_routes import decision_router
|
||||
from api.passkey_routes import passkey_router
|
||||
from api.perf_routes import perf_router
|
||||
from api.poi_routes import poi_router
|
||||
from api.ws_routes import ws_router
|
||||
from api.rate_limit_config import RateLimitConfig
|
||||
|
|
@ -105,6 +106,7 @@ app = FastAPI(
|
|||
openapi_url=None if APP_ENV == "production" else "/openapi.json",
|
||||
)
|
||||
app.include_router(passkey_router)
|
||||
app.include_router(perf_router)
|
||||
app.include_router(poi_router)
|
||||
app.include_router(decision_router)
|
||||
app.include_router(ws_router)
|
||||
|
|
|
|||
|
|
@ -56,6 +56,14 @@ celery_tasks_total: Counter
|
|||
celery_task_duration_seconds: Histogram
|
||||
celery_tasks_active: UpDownCounter
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Frontend performance metrics
|
||||
# ---------------------------------------------------------------------------
|
||||
frontend_worker_roundtrip: Histogram
|
||||
frontend_worker_compute: Histogram
|
||||
frontend_main_thread: Histogram
|
||||
frontend_feature_count: Histogram
|
||||
|
||||
|
||||
def init_metrics(service_name: str = "realestate-crawler") -> PrometheusMetricReader:
|
||||
"""Initialise the OTel MeterProvider and define all instruments.
|
||||
|
|
@ -70,6 +78,8 @@ def init_metrics(service_name: str = "realestate-crawler") -> PrometheusMetricRe
|
|||
global geojson_cache_operations
|
||||
global ocr_attempts, ocr_successes
|
||||
global celery_tasks_total, celery_task_duration_seconds, celery_tasks_active
|
||||
global frontend_worker_roundtrip, frontend_worker_compute
|
||||
global frontend_main_thread, frontend_feature_count
|
||||
|
||||
if _reader is not None:
|
||||
return _reader
|
||||
|
|
@ -144,6 +154,24 @@ def init_metrics(service_name: str = "realestate-crawler") -> PrometheusMetricRe
|
|||
description="Currently active Celery tasks",
|
||||
)
|
||||
|
||||
# -- Frontend performance --
|
||||
frontend_worker_roundtrip = _meter.create_histogram(
|
||||
"frontend_worker_roundtrip_seconds",
|
||||
description="Browser worker message round-trip time",
|
||||
)
|
||||
frontend_worker_compute = _meter.create_histogram(
|
||||
"frontend_worker_compute_seconds",
|
||||
description="Computation time inside the web worker",
|
||||
)
|
||||
frontend_main_thread = _meter.create_histogram(
|
||||
"frontend_main_thread_seconds",
|
||||
description="Main-thread blocking operation duration",
|
||||
)
|
||||
frontend_feature_count = _meter.create_histogram(
|
||||
"frontend_feature_count",
|
||||
description="Number of features per heatmap load",
|
||||
)
|
||||
|
||||
return _reader
|
||||
|
||||
|
||||
|
|
|
|||
43
api/perf_routes.py
Normal file
43
api/perf_routes.py
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
"""Frontend performance metrics ingestion endpoint."""
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
|
||||
import api.metrics as app_metrics
|
||||
|
||||
ALLOWED_METRICS = {"worker_roundtrip", "worker_compute", "main_thread", "feature_count"}
|
||||
MAX_BATCH_SIZE = 100
|
||||
|
||||
|
||||
class PerfSample(BaseModel):
|
||||
metric: str
|
||||
operation: str = Field(max_length=50)
|
||||
value: float = Field(ge=0, le=3600)
|
||||
|
||||
@field_validator("metric")
|
||||
@classmethod
|
||||
def validate_metric(cls, v: str) -> str:
|
||||
if v not in ALLOWED_METRICS:
|
||||
raise ValueError(f"Unknown metric: {v}")
|
||||
return v
|
||||
|
||||
|
||||
perf_router = APIRouter(tags=["perf"])
|
||||
|
||||
|
||||
@perf_router.post("/api/perf", status_code=204)
|
||||
async def record_perf(samples: list[PerfSample]) -> None:
|
||||
if len(samples) > MAX_BATCH_SIZE:
|
||||
samples = samples[:MAX_BATCH_SIZE]
|
||||
|
||||
for s in samples:
|
||||
attrs = {"operation": s.operation}
|
||||
if s.metric == "worker_roundtrip":
|
||||
app_metrics.frontend_worker_roundtrip.record(s.value, attrs)
|
||||
elif s.metric == "worker_compute":
|
||||
app_metrics.frontend_worker_compute.record(s.value, attrs)
|
||||
elif s.metric == "main_thread":
|
||||
app_metrics.frontend_main_thread.record(s.value, attrs)
|
||||
elif s.metric == "feature_count":
|
||||
app_metrics.frontend_feature_count.record(s.value)
|
||||
Loading…
Add table
Add a link
Reference in a new issue