Coverage for app / services / ping_service.py: 100%
41 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-05 17:54 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-05 17:54 +0000
1import httpx
2from datetime import datetime, timezone
3from typing import Optional, Tuple
5from app.services.alert_service import AlertService
8class PingService:
9 """Handles HTTP health checks and result processing."""
11 DEFAULT_TIMEOUT = 10.0
13 # Browser-like headers to avoid bot detection
14 _HEADERS = {
15 "User-Agent": (
16 "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
17 "AppleWebKit/537.36 (KHTML, like Gecko) "
18 "Chrome/124.0.0.0 Safari/537.36"
19 ),
20 "Accept": (
21 "text/html,application/xhtml+xml,application/xml;"
22 "q=0.9,image/avif,image/webp,*/*;q=0.8"
23 ),
24 "Accept-Language": "en-US,en;q=0.5",
25 "Accept-Encoding": "gzip, deflate, br",
26 "Sec-Fetch-Dest": "document",
27 "Sec-Fetch-Mode": "navigate",
28 "Sec-Fetch-Site": "none",
29 "Sec-Fetch-User": "?1",
30 "Upgrade-Insecure-Requests": "1",
31 }
33 def __init__(self, alert_service: Optional[AlertService] = None):
34 self.alert_service = alert_service
36 def check_url(self, url: str) -> Tuple[bool, Optional[int], Optional[int], Optional[str]]:
37 """
38 Perform HTTP GET against URL.
39 Returns: (is_up, status_code, response_ms, error_message)
40 """
41 start = datetime.now(timezone.utc)
42 is_up = False
43 status_code = None
44 response_ms = None
45 error_message = None
47 try:
48 with httpx.Client(
49 timeout=self.DEFAULT_TIMEOUT,
50 follow_redirects=True,
51 headers=self._HEADERS,
52 ) as client:
53 response = client.get(url)
54 status_code = response.status_code
55 is_up = 200 <= status_code < 400
56 response_ms = int(
57 (datetime.now(timezone.utc) - start).total_seconds() * 1000
58 )
59 except httpx.TimeoutException:
60 error_message = "Request timed out"
61 is_up = False
62 except httpx.ConnectError:
63 error_message = "Connection failed"
64 is_up = False
65 except Exception as e:
66 error_message = str(e)
67 is_up = False
69 return is_up, status_code, response_ms, error_message
71 def process_monitor_check(self, monitor_id: str, url: str, db) -> dict:
72 """
73 Full check pipeline: ping URL, record result, handle alerts.
74 Returns result summary dict.
75 """
76 is_up, status_code, response_ms, error_message = self.check_url(url)
78 if self.alert_service:
79 alert_status = self.alert_service.process_ping_result(
80 monitor_id=monitor_id,
81 is_up=is_up,
82 status_code=status_code,
83 response_ms=response_ms,
84 error_message=error_message,
85 )
86 else:
87 # Fallback: just record ping without alerts
88 from app.models.ping_log import PingLog
89 ping = PingLog(
90 monitor_id=monitor_id,
91 status_code=status_code,
92 response_ms=response_ms,
93 is_up=is_up,
94 error_message=error_message,
95 )
96 db.add(ping)
97 db.commit()
98 alert_status = None
100 return {
101 "monitor_id": monitor_id,
102 "is_up": is_up,
103 "status_code": status_code,
104 "response_ms": response_ms,
105 "error_message": error_message,
106 "alert_sent": alert_status,
107 }