stats.py 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  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("SELECT COUNT(*) FROM access_logs") or 0
  31. active_ips: int = (
  32. await conn.fetchval(
  33. "SELECT COUNT(DISTINCT ip) FROM access_logs "
  34. "WHERE created_at > NOW() - INTERVAL '5 minutes'"
  35. )
  36. or 0
  37. )
  38. avg_latency = await conn.fetchval("SELECT AVG(latency_ms) FROM access_logs")
  39. return StatsOut(
  40. uptime_seconds=time.time() - START_TIME,
  41. total_hits=total_hits,
  42. active_ips=active_ips,
  43. avg_latency_ms=round(float(avg_latency), 2) if avg_latency is not None else 0.0,
  44. )
  45. @router.get("/geo/distribution", response_model=List[GeoDistributionItem])
  46. async def get_geo_distribution() -> List[GeoDistributionItem]:
  47. pool = get_pool()
  48. async with pool.acquire() as conn:
  49. rows = await conn.fetch(
  50. "SELECT country, COUNT(*) AS cnt FROM access_logs "
  51. "GROUP BY country ORDER BY cnt DESC"
  52. )
  53. total = sum(r["cnt"] for r in rows)
  54. return [
  55. GeoDistributionItem(
  56. country=row["country"],
  57. count=row["cnt"],
  58. percentage=round(row["cnt"] / total * 100, 2) if total else 0.0,
  59. )
  60. for row in rows
  61. ]
  62. @router.get("/prices/top-ips", response_model=List[dict])
  63. async def get_top_price_ips() -> List[dict]:
  64. pool = get_pool()
  65. async with pool.acquire() as conn:
  66. rows = await conn.fetch(
  67. """
  68. SELECT ip, COUNT(*) AS hit_count
  69. FROM access_logs
  70. WHERE path LIKE '/api/public/prices%'
  71. GROUP BY ip
  72. ORDER BY hit_count DESC
  73. LIMIT 20
  74. """
  75. )
  76. total = sum(r["hit_count"] for r in rows) or 1
  77. return [
  78. {
  79. "ip": r["ip"],
  80. "hit_count": r["hit_count"],
  81. "percentage": round(r["hit_count"] / total * 100, 2),
  82. }
  83. for r in rows
  84. ]
  85. @router.get("/geo/points", response_model=List[GeoPoint])
  86. async def get_geo_points() -> List[GeoPoint]:
  87. pool = get_pool()
  88. async with pool.acquire() as conn:
  89. rows = await conn.fetch(
  90. "SELECT latitude, longitude, country, city, COUNT(*) AS hit_count "
  91. "FROM access_logs "
  92. "WHERE latitude IS NOT NULL AND longitude IS NOT NULL "
  93. "GROUP BY latitude, longitude, country, city "
  94. "ORDER BY hit_count DESC "
  95. "LIMIT 1000"
  96. )
  97. return [
  98. GeoPoint(
  99. latitude=row["latitude"],
  100. longitude=row["longitude"],
  101. country=row["country"],
  102. city=row["city"],
  103. hit_count=row["hit_count"],
  104. )
  105. for row in rows
  106. ]