stats.py 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. from __future__ import annotations
  2. import time
  3. from typing import List
  4. from fastapi import APIRouter
  5. from pydantic import BaseModel
  6. from app.db import get_pool
  7. START_TIME = time.time()
  8. router = APIRouter(tags=["stats"])
  9. # ---------- Pydantic models ----------
  10. class StatsOut(BaseModel):
  11. uptime_seconds: float
  12. total_hits: int
  13. active_ips: int
  14. avg_latency_ms: float
  15. class GeoDistributionItem(BaseModel):
  16. country: str
  17. count: int
  18. percentage: float
  19. class GeoPoint(BaseModel):
  20. latitude: float
  21. longitude: float
  22. country: str
  23. city: str
  24. hit_count: int
  25. # ---------- Endpoints ----------
  26. @router.get("/stats", response_model=StatsOut)
  27. async def get_stats() -> StatsOut:
  28. pool = get_pool()
  29. async with pool.acquire() as conn:
  30. total_hits: int = await conn.fetchval(
  31. "SELECT COUNT(*) FROM access_logs WHERE path LIKE '/api/public/prices%'"
  32. ) or 0
  33. active_ips: int = (
  34. await conn.fetchval(
  35. "SELECT COUNT(DISTINCT ip) FROM access_logs "
  36. "WHERE created_at > NOW() - INTERVAL '5 minutes'"
  37. )
  38. or 0
  39. )
  40. avg_latency = await conn.fetchval("SELECT AVG(latency_ms) FROM access_logs")
  41. return StatsOut(
  42. uptime_seconds=time.time() - START_TIME,
  43. total_hits=total_hits,
  44. active_ips=active_ips,
  45. avg_latency_ms=round(float(avg_latency), 2) if avg_latency is not None else 0.0,
  46. )
  47. @router.get("/geo/distribution", response_model=List[GeoDistributionItem])
  48. async def get_geo_distribution() -> List[GeoDistributionItem]:
  49. pool = get_pool()
  50. async with pool.acquire() as conn:
  51. rows = await conn.fetch(
  52. "SELECT country, COUNT(*) AS cnt FROM access_logs "
  53. "GROUP BY country ORDER BY cnt DESC"
  54. )
  55. total = sum(r["cnt"] for r in rows)
  56. return [
  57. GeoDistributionItem(
  58. country=row["country"],
  59. count=row["cnt"],
  60. percentage=round(row["cnt"] / total * 100, 2) if total else 0.0,
  61. )
  62. for row in rows
  63. ]
  64. @router.get("/prices/top-ips", response_model=List[dict])
  65. async def get_top_price_ips() -> List[dict]:
  66. pool = get_pool()
  67. async with pool.acquire() as conn:
  68. rows = await conn.fetch(
  69. """
  70. SELECT ip, COUNT(*) AS hit_count
  71. FROM access_logs
  72. WHERE path LIKE '/api/public/prices%'
  73. GROUP BY ip
  74. ORDER BY hit_count DESC
  75. LIMIT 20
  76. """
  77. )
  78. total = sum(r["hit_count"] for r in rows) or 1
  79. return [
  80. {
  81. "ip": r["ip"],
  82. "hit_count": r["hit_count"],
  83. "percentage": round(r["hit_count"] / total * 100, 2),
  84. }
  85. for r in rows
  86. ]
  87. @router.get("/geo/points", response_model=List[GeoPoint])
  88. async def get_geo_points() -> List[GeoPoint]:
  89. pool = get_pool()
  90. async with pool.acquire() as conn:
  91. rows = await conn.fetch(
  92. "SELECT latitude, longitude, country, city, COUNT(*) AS hit_count "
  93. "FROM access_logs "
  94. "WHERE latitude IS NOT NULL AND longitude IS NOT NULL "
  95. "GROUP BY latitude, longitude, country, city "
  96. "ORDER BY hit_count DESC "
  97. "LIMIT 1000"
  98. )
  99. return [
  100. GeoPoint(
  101. latitude=row["latitude"],
  102. longitude=row["longitude"],
  103. country=row["country"],
  104. city=row["city"],
  105. hit_count=row["hit_count"],
  106. )
  107. for row in rows
  108. ]