Add Server-Timing headers to all API endpoints for per-request latency breakdown

Instrument every API endpoint with Server-Timing headers so sub-operation
durations are visible in browser DevTools Network tab. Also adds Grafana
dashboard panels for per-endpoint latency comparison (p50/p95 timeseries
and p95 ranking bar gauge).
This commit is contained in:
Viktor Barzin 2026-02-23 21:30:51 +00:00
parent 35f1987ac1
commit 2357722e80
No known key found for this signature in database
GPG key ID: 0EB088298288D958
4 changed files with 271 additions and 5 deletions

View file

@ -1,7 +1,8 @@
import logging
import time
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException
from fastapi import APIRouter, Depends, HTTPException, Response
from pydantic import BaseModel, Field
from api.auth import User, get_current_user
@ -79,11 +80,20 @@ def _poi_to_response(poi: "poi_service.PointOfInterest") -> POIResponse:
@poi_router.get("", response_model=list[POIResponse])
async def list_pois(
user: Annotated[User, Depends(get_current_user)],
response: Response,
) -> list[POIResponse]:
"""List all POIs for the current user."""
timings: list[str] = []
t0_total = time.monotonic()
t0 = time.monotonic()
user_id = _get_user_id(user)
timings.append(f"user_lookup;dur={(time.monotonic() - t0) * 1000:.1f}")
repo = POIRepository(engine)
t0 = time.monotonic()
pois = poi_service.get_user_pois(repo, user_id)
timings.append(f"fetch_pois;dur={(time.monotonic() - t0) * 1000:.1f}")
timings.append(f"total;dur={(time.monotonic() - t0_total) * 1000:.1f}")
response.headers["Server-Timing"] = ", ".join(timings)
return [_poi_to_response(p) for p in pois]
@ -91,10 +101,16 @@ async def list_pois(
async def create_poi(
user: Annotated[User, Depends(get_current_user)],
body: CreatePOIRequest,
response: Response,
) -> POIResponse:
"""Create a new POI."""
timings: list[str] = []
t0_total = time.monotonic()
t0 = time.monotonic()
user_id = _get_user_id(user)
timings.append(f"user_lookup;dur={(time.monotonic() - t0) * 1000:.1f}")
repo = POIRepository(engine)
t0 = time.monotonic()
result = poi_service.create_poi(
repo,
user_id=user_id,
@ -103,6 +119,9 @@ async def create_poi(
latitude=body.latitude,
longitude=body.longitude,
)
timings.append(f"create;dur={(time.monotonic() - t0) * 1000:.1f}")
timings.append(f"total;dur={(time.monotonic() - t0_total) * 1000:.1f}")
response.headers["Server-Timing"] = ", ".join(timings)
return _poi_to_response(result.poi)
@ -111,10 +130,16 @@ async def update_poi(
user: Annotated[User, Depends(get_current_user)],
poi_id: int,
body: UpdatePOIRequest,
response: Response,
) -> POIResponse:
"""Update an existing POI."""
timings: list[str] = []
t0_total = time.monotonic()
t0 = time.monotonic()
user_id = _get_user_id(user)
timings.append(f"user_lookup;dur={(time.monotonic() - t0) * 1000:.1f}")
repo = POIRepository(engine)
t0 = time.monotonic()
result = poi_service.update_poi(
repo,
poi_id=poi_id,
@ -124,8 +149,11 @@ async def update_poi(
latitude=body.latitude,
longitude=body.longitude,
)
timings.append(f"update;dur={(time.monotonic() - t0) * 1000:.1f}")
if result is None:
raise HTTPException(status_code=404, detail="POI not found")
timings.append(f"total;dur={(time.monotonic() - t0_total) * 1000:.1f}")
response.headers["Server-Timing"] = ", ".join(timings)
return _poi_to_response(result.poi)
@ -133,13 +161,22 @@ async def update_poi(
async def delete_poi(
user: Annotated[User, Depends(get_current_user)],
poi_id: int,
response: Response,
) -> dict[str, bool]:
"""Delete a POI and its associated distances."""
timings: list[str] = []
t0_total = time.monotonic()
t0 = time.monotonic()
user_id = _get_user_id(user)
timings.append(f"user_lookup;dur={(time.monotonic() - t0) * 1000:.1f}")
repo = POIRepository(engine)
t0 = time.monotonic()
deleted = poi_service.delete_poi(repo, poi_id, user_id)
timings.append(f"delete;dur={(time.monotonic() - t0) * 1000:.1f}")
if not deleted:
raise HTTPException(status_code=404, detail="POI not found")
timings.append(f"total;dur={(time.monotonic() - t0_total) * 1000:.1f}")
response.headers["Server-Timing"] = ", ".join(timings)
return {"success": True}
@ -148,16 +185,24 @@ async def calculate_distances(
user: Annotated[User, Depends(get_current_user)],
poi_id: int,
body: CalculateRequest,
response: Response,
) -> dict[str, str]:
"""Trigger distance calculation for a POI."""
timings: list[str] = []
t0_total = time.monotonic()
t0 = time.monotonic()
user_id = _get_user_id(user)
timings.append(f"user_lookup;dur={(time.monotonic() - t0) * 1000:.1f}")
repo = POIRepository(engine)
# Verify POI exists and belongs to user
t0 = time.monotonic()
poi = poi_service.get_poi(repo, poi_id)
if poi is None or poi.user_id != user_id:
raise HTTPException(status_code=404, detail="POI not found")
timings.append(f"verify_poi;dur={(time.monotonic() - t0) * 1000:.1f}")
t0 = time.monotonic()
result = poi_service.trigger_calculation(
poi_id=poi_id,
travel_modes=body.travel_modes,
@ -165,33 +210,51 @@ async def calculate_distances(
user_email=user.email,
listing_ids=body.listing_ids,
)
timings.append(f"trigger;dur={(time.monotonic() - t0) * 1000:.1f}")
if result.task_id:
task_service.add_task_for_user(user.email, result.task_id)
timings.append(f"total;dur={(time.monotonic() - t0_total) * 1000:.1f}")
response.headers["Server-Timing"] = ", ".join(timings)
return {"task_id": result.task_id or "", "message": result.message}
@poi_router.get("/distances/bulk")
async def get_bulk_distances(
user: Annotated[User, Depends(get_current_user)],
response: Response,
listing_type: ListingType = ListingType.RENT,
) -> dict[int, list[POIDistanceResponse]]:
"""Get all POI distances for the current user, keyed by listing ID."""
timings: list[str] = []
t0_total = time.monotonic()
t0 = time.monotonic()
user_id = _get_user_id(user)
timings.append(f"user_lookup;dur={(time.monotonic() - t0) * 1000:.1f}")
repo = POIRepository(engine)
t0 = time.monotonic()
pois = {p.id: p for p in poi_service.get_user_pois(repo, user_id)}
timings.append(f"fetch_pois;dur={(time.monotonic() - t0) * 1000:.1f}")
if not pois:
timings.append(f"total;dur={(time.monotonic() - t0_total) * 1000:.1f}")
response.headers["Server-Timing"] = ", ".join(timings)
return {}
from repositories.listing_repository import ListingRepository
from database import engine as db_engine
listing_repo = ListingRepository(db_engine)
t0 = time.monotonic()
all_ids = list(listing_repo.get_listing_ids(listing_type))
timings.append(f"fetch_ids;dur={(time.monotonic() - t0) * 1000:.1f}")
if not all_ids:
timings.append(f"total;dur={(time.monotonic() - t0_total) * 1000:.1f}")
response.headers["Server-Timing"] = ", ".join(timings)
return {}
t0 = time.monotonic()
distances = repo.get_distances_for_listings(all_ids, listing_type, user_id)
timings.append(f"fetch_distances;dur={(time.monotonic() - t0) * 1000:.1f}")
result: dict[int, list[POIDistanceResponse]] = {}
for d in distances:
@ -205,6 +268,8 @@ async def get_bulk_distances(
distance_meters=d.distance_meters,
)
)
timings.append(f"total;dur={(time.monotonic() - t0_total) * 1000:.1f}")
response.headers["Server-Timing"] = ", ".join(timings)
return result
@ -212,19 +277,30 @@ async def get_bulk_distances(
async def get_distances(
user: Annotated[User, Depends(get_current_user)],
listing_id: int,
response: Response,
listing_type: ListingType = ListingType.RENT,
) -> list[POIDistanceResponse]:
"""Get POI distances for a specific listing."""
timings: list[str] = []
t0_total = time.monotonic()
t0 = time.monotonic()
user_id = _get_user_id(user)
timings.append(f"user_lookup;dur={(time.monotonic() - t0) * 1000:.1f}")
repo = POIRepository(engine)
t0 = time.monotonic()
poi_repo_pois = {
p.id: p for p in poi_service.get_user_pois(repo, user_id)
}
timings.append(f"fetch_pois;dur={(time.monotonic() - t0) * 1000:.1f}")
t0 = time.monotonic()
distances = poi_service.get_distances_for_listing(
repo, listing_id, listing_type, user_id
)
timings.append(f"fetch_distances;dur={(time.monotonic() - t0) * 1000:.1f}")
timings.append(f"total;dur={(time.monotonic() - t0_total) * 1000:.1f}")
response.headers["Server-Timing"] = ", ".join(timings)
return [
POIDistanceResponse(
poi_id=d.poi_id,