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, Query
from fastapi import APIRouter, Depends, HTTPException, Query, Response
from pydantic import BaseModel, Field
from api.auth import User, get_current_user
@ -55,11 +56,17 @@ async def set_decision(
user: Annotated[User, Depends(get_current_user)],
listing_id: int,
body: SetDecisionRequest,
response: Response,
) -> DecisionResponse:
"""Set or update a like/dislike decision for a 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 = DecisionRepository(engine)
try:
t0 = time.monotonic()
result = decision_service.set_decision(
repo,
user_id=user_id,
@ -67,19 +74,31 @@ async def set_decision(
listing_type=body.listing_type,
decision=body.decision,
)
timings.append(f"upsert;dur={(time.monotonic() - t0) * 1000:.1f}")
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
timings.append(f"total;dur={(time.monotonic() - t0_total) * 1000:.1f}")
response.headers["Server-Timing"] = ", ".join(timings)
return _to_response(result)
@decision_router.get("", response_model=list[DecisionResponse])
async def get_decisions(
user: Annotated[User, Depends(get_current_user)],
response: Response,
) -> list[DecisionResponse]:
"""Get all decisions 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 = DecisionRepository(engine)
t0 = time.monotonic()
decisions = decision_service.get_user_decisions(repo, user_id)
timings.append(f"fetch;dur={(time.monotonic() - t0) * 1000:.1f}")
timings.append(f"total;dur={(time.monotonic() - t0_total) * 1000:.1f}")
response.headers["Server-Timing"] = ", ".join(timings)
return [_to_response(d) for d in decisions]
@ -87,17 +106,26 @@ async def get_decisions(
async def delete_decision(
user: Annotated[User, Depends(get_current_user)],
listing_id: int,
response: Response,
listing_type: str = Query(..., description="RENT or BUY"),
) -> dict[str, bool]:
"""Remove a decision (un-like/un-dislike)."""
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 = DecisionRepository(engine)
try:
t0 = time.monotonic()
deleted = decision_service.remove_decision(
repo, user_id, listing_id, listing_type
)
timings.append(f"delete;dur={(time.monotonic() - t0) * 1000:.1f}")
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
if not deleted:
raise HTTPException(status_code=404, detail="Decision not found")
timings.append(f"total;dur={(time.monotonic() - t0_total) * 1000:.1f}")
response.headers["Server-Timing"] = ", ".join(timings)
return {"success": True}