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:
parent
35f1987ac1
commit
2357722e80
4 changed files with 271 additions and 5 deletions
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue