Functional Weave
Code in Python

monitor.http-check-result@1.0.0

impl/python.py

3,857 bytes · the Python implementation · view raw

from typing import Optional, Sequence

from .monitor_http_check_result_types import CheckOutcome, HttpExpectation, HttpProbe

_ERROR_MESSAGES = {
    "timeout": "timed out",
    "dns": "DNS lookup failed",
    "connection": "connection failed",
    "tls": "TLS handshake failed",
    "protocol": "protocol error",
}


def _whole(v: object) -> bool:
    return isinstance(v, int) and not isinstance(v, bool)


def _expected_text(statuses: Optional[Sequence[int]]) -> str:
    if statuses is None:
        return "2xx"
    if len(statuses) == 1:
        return str(statuses[0])
    return "%s or %d" % (", ".join(str(s) for s in statuses[:-1]), statuses[-1])


def _threshold(name: str, v: Optional[int]) -> None:
    if v is not None and (not _whole(v) or v < 1):
        raise ValueError("%s must be a whole number 1 or more, received %r" % (name, v))


def classify_http_check(probe: HttpProbe, expect: HttpExpectation) -> CheckOutcome:
    """One probe, one verdict, checked in a fixed order so the reason is the
    most fundamental thing wrong: a transport error, then the status code,
    then the body, then latency. A 503 that also took ten seconds is
    reported as the 503, because that is what someone has to fix."""
    status_code, latency_ms, body, error = probe.status_code, probe.latency_ms, probe.body, probe.error
    if status_code is not None and (not _whole(status_code) or status_code < 100 or status_code > 599):
        raise ValueError("statusCode must be 100 to 599, received %r" % (status_code,))
    if error is not None and error not in _ERROR_MESSAGES:
        raise ValueError("unknown probe error: %s" % (error,))
    if status_code is None and error is None:
        raise ValueError("probe must have a statusCode or an error")
    if latency_ms is not None and (not _whole(latency_ms) or latency_ms < 0):
        raise ValueError("latencyMs must be a whole number 0 or more, received %r" % (latency_ms,))
    statuses, body_contains = expect.statuses, expect.body_contains
    degraded, down = expect.degraded_latency_ms, expect.down_latency_ms
    if statuses is not None:
        if len(statuses) == 0:
            raise ValueError("statuses must not be empty; use null to accept any 2xx")
        for s in statuses:
            if not _whole(s) or s < 100 or s > 599:
                raise ValueError("expected statuses must be 100 to 599, received %r" % (s,))
    _threshold("degradedLatencyMs", degraded)
    _threshold("downLatencyMs", down)
    if degraded is not None and down is not None and down < degraded:
        raise ValueError("downLatencyMs must not be below degradedLatencyMs: %d < %d" % (down, degraded))

    if error is not None:
        return CheckOutcome(status="down", reason=error, message=_ERROR_MESSAGES[error])
    assert status_code is not None
    accepted = 200 <= status_code <= 299 if statuses is None else status_code in statuses
    if not accepted:
        return CheckOutcome(status="down", reason="unexpected-status", message="HTTP %d, expected %s" % (status_code, _expected_text(statuses)))
    if body_contains is not None and (body is None or body_contains not in body):
        return CheckOutcome(status="down", reason="body-mismatch", message='body does not contain "%s"' % (body_contains,))
    if latency_ms is not None:
        if down is not None and latency_ms >= down:
            return CheckOutcome(status="down", reason="too-slow", message="latency %d ms at or above %d ms" % (latency_ms, down))
        if degraded is not None and latency_ms >= degraded:
            return CheckOutcome(status="degraded", reason="slow", message="latency %d ms at or above %d ms" % (latency_ms, degraded))
    message = "HTTP %d" % status_code if latency_ms is None else "HTTP %d in %d ms" % (status_code, latency_ms)
    return CheckOutcome(status="up", reason="ok", message=message)