Files
crucible-agent-build-fastap…/app/checker.py

90 lines
3.3 KiB
Python

import logging
from time import perf_counter
from urllib.parse import urljoin
import httpx
from app.models import CheckResult, CurrentStatus, Monitor, State, now_utc
from app.security import Resolver, UnsafeTargetError, redacted_url, resolve_addresses, validate_target
logger = logging.getLogger(__name__)
REDIRECTS = {301, 302, 303, 307, 308}
class EndpointChecker:
def __init__(
self,
timeout_seconds: float,
max_redirects: int,
resolver: Resolver = resolve_addresses,
transport: httpx.AsyncBaseTransport | None = None,
) -> None:
self.timeout_seconds = timeout_seconds
self.max_redirects = max_redirects
self.resolver = resolver
self.transport = transport
async def check(self, monitor: Monitor) -> CheckResult:
started = perf_counter()
url = str(monitor.url)
state = State.ERROR
observed: int | None = None
error: str | None = None
try:
async with httpx.AsyncClient(
transport=self.transport,
timeout=httpx.Timeout(self.timeout_seconds),
follow_redirects=False,
) as client:
for hop in range(self.max_redirects + 1):
await validate_target(url, self.resolver)
async with client.stream(
"GET", url, headers={"user-agent": "endpoint-monitor/0.1"}
) as response:
observed = response.status_code
if observed not in REDIRECTS:
state = (
State.UP if observed == monitor.expected_status else State.DOWN
)
break
location = response.headers.get("location")
if not location:
error = "redirect response omitted Location"
break
if hop == self.max_redirects:
error = "redirect limit exceeded"
break
url = urljoin(url, location)
except UnsafeTargetError:
state = State.BLOCKED
error = "target blocked by outbound request policy"
except httpx.TimeoutException:
error = "outbound request timed out"
except httpx.HTTPError:
error = "outbound HTTP request failed"
except OSError:
error = "outbound network operation failed"
latency = round((perf_counter() - started) * 1000, 3)
status = CurrentStatus(
state=state,
checked_at=now_utc(),
observed_status=observed,
latency_ms=latency,
error=error,
)
logger.info(
"endpoint_check",
extra={
"event_data": {
"event": "endpoint_check",
"monitor_id": str(monitor.id),
"url": redacted_url(str(monitor.url)),
"state": state.value,
"observed_status": observed,
"latency_ms": latency,
}
},
)
return CheckResult(monitor_id=monitor.id, **status.model_dump())