feat: ping and DNS monitor coroutines
This commit is contained in:
@@ -0,0 +1,71 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import socket
|
||||||
|
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from fablednetmon.core.alerts import record_metric
|
||||||
|
from fablednetmon.models.hosts import Host
|
||||||
|
from fablednetmon.models.monitors import DnsResult, DnsStatus
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def dns_check(host: Host, session: AsyncSession) -> None:
|
||||||
|
"""Resolve host and write dns_result + metrics."""
|
||||||
|
result = await _resolve(host.address)
|
||||||
|
|
||||||
|
if result["resolved"]:
|
||||||
|
resolved_ip = result["ip"] # first A/AAAA record (stored in DB)
|
||||||
|
all_ips = result["all_ips"]
|
||||||
|
|
||||||
|
# Check against expected IP: pass only if at least one returned record matches
|
||||||
|
if host.dns_expected_ip and host.dns_expected_ip not in all_ips:
|
||||||
|
status = DnsStatus.failed
|
||||||
|
dns_resolved_metric = 0.0
|
||||||
|
else:
|
||||||
|
status = DnsStatus.resolved
|
||||||
|
dns_resolved_metric = 1.0
|
||||||
|
|
||||||
|
# Detect IP change vs most recent successful resolution
|
||||||
|
prev_result = await session.execute(
|
||||||
|
select(DnsResult)
|
||||||
|
.where(DnsResult.host_id == host.id, DnsResult.status == DnsStatus.resolved)
|
||||||
|
.order_by(DnsResult.resolved_at.desc())
|
||||||
|
.limit(1)
|
||||||
|
)
|
||||||
|
prev = prev_result.scalar_one_or_none()
|
||||||
|
ip_changed = 1.0 if (prev and prev.resolved_ip != resolved_ip) else 0.0
|
||||||
|
else:
|
||||||
|
resolved_ip = None
|
||||||
|
status = DnsStatus.failed
|
||||||
|
dns_resolved_metric = 0.0
|
||||||
|
ip_changed = 0.0
|
||||||
|
|
||||||
|
session.add(DnsResult(
|
||||||
|
host_id=host.id,
|
||||||
|
status=status,
|
||||||
|
resolved_ip=resolved_ip,
|
||||||
|
))
|
||||||
|
await session.flush()
|
||||||
|
|
||||||
|
await record_metric(session, "dns", host.name, "resolved", dns_resolved_metric)
|
||||||
|
await record_metric(session, "dns", host.name, "ip_changed", ip_changed)
|
||||||
|
|
||||||
|
|
||||||
|
async def _resolve(address: str) -> dict:
|
||||||
|
"""Resolve hostname. Returns all A/AAAA records; stores first as resolved_ip."""
|
||||||
|
try:
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
infos = await loop.run_in_executor(
|
||||||
|
None, socket.getaddrinfo, address, None, socket.AF_UNSPEC, socket.SOCK_STREAM
|
||||||
|
)
|
||||||
|
if infos:
|
||||||
|
all_ips = [info[4][0] for info in infos]
|
||||||
|
return {"resolved": True, "ip": all_ips[0], "all_ips": all_ips}
|
||||||
|
return {"resolved": False, "ip": None, "all_ips": []}
|
||||||
|
except Exception as exc:
|
||||||
|
logger.debug(f"DNS resolution failed for {address}: {exc}")
|
||||||
|
return {"resolved": False, "ip": None, "all_ips": []}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from fablednetmon.core.alerts import record_metric
|
||||||
|
from fablednetmon.models.hosts import Host, ProbeType
|
||||||
|
from fablednetmon.models.monitors import PingResult, PingStatus
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
TCP_TIMEOUT = 5.0
|
||||||
|
DEFAULT_TCP_PORT = 80
|
||||||
|
|
||||||
|
|
||||||
|
async def ping_check(host: Host, session: AsyncSession) -> None:
|
||||||
|
"""Probe a single host and write ping_result + metrics."""
|
||||||
|
if host.probe_type == ProbeType.icmp:
|
||||||
|
result = await _icmp_ping(host.address)
|
||||||
|
else:
|
||||||
|
result = await _tcp_ping(host.address, host.probe_port or DEFAULT_TCP_PORT)
|
||||||
|
|
||||||
|
status = PingStatus.up if result["up"] else PingStatus.down
|
||||||
|
session.add(PingResult(
|
||||||
|
host_id=host.id,
|
||||||
|
status=status,
|
||||||
|
response_time_ms=result.get("response_time_ms"),
|
||||||
|
))
|
||||||
|
await session.flush()
|
||||||
|
|
||||||
|
await record_metric(session, "ping", host.name, "up", 1.0 if result["up"] else 0.0)
|
||||||
|
await record_metric(
|
||||||
|
session, "ping", host.name, "response_time_ms",
|
||||||
|
result.get("response_time_ms") or 0.0,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _tcp_ping(address: str, port: int) -> dict:
|
||||||
|
start = time.monotonic()
|
||||||
|
try:
|
||||||
|
reader, writer = await asyncio.wait_for(
|
||||||
|
asyncio.open_connection(address, port),
|
||||||
|
timeout=TCP_TIMEOUT,
|
||||||
|
)
|
||||||
|
writer.close()
|
||||||
|
await writer.wait_closed()
|
||||||
|
return {"up": True, "response_time_ms": round((time.monotonic() - start) * 1000, 2)}
|
||||||
|
except Exception:
|
||||||
|
return {"up": False, "response_time_ms": None}
|
||||||
|
|
||||||
|
|
||||||
|
async def _icmp_ping(address: str) -> dict:
|
||||||
|
"""Use system ping command (setuid binary; no raw socket privilege needed in Python)."""
|
||||||
|
start = time.monotonic()
|
||||||
|
try:
|
||||||
|
proc = await asyncio.create_subprocess_exec(
|
||||||
|
"ping", "-c", "1", "-W", "2", address,
|
||||||
|
stdout=asyncio.subprocess.DEVNULL,
|
||||||
|
stderr=asyncio.subprocess.DEVNULL,
|
||||||
|
)
|
||||||
|
await asyncio.wait_for(proc.wait(), timeout=5.0)
|
||||||
|
if proc.returncode == 0:
|
||||||
|
return {"up": True, "response_time_ms": round((time.monotonic() - start) * 1000, 2)}
|
||||||
|
return {"up": False, "response_time_ms": None}
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning(f"ICMP ping failed for {address}: {exc}, falling back to TCP")
|
||||||
|
return await _tcp_ping(address, DEFAULT_TCP_PORT)
|
||||||
Reference in New Issue
Block a user