stats.py 3.9 KB

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