Frontend Labs
⋯

46. Project: an async service health checker

Goal: check a list of endpoints concurrently every N seconds: DNS time, TCP connect time, TLS handshake time and certificate expiry, HTTP status, and total latency. Print a table and exit non-zero if anything is down, so cron or CI can alert. It reuses every chapter in this Part.

Only point it at systems you own or are allowed to test.

# labs/healthcheck.py
import asyncio
import socket
import ssl
import sys
import time
from dataclasses import dataclass, field
from datetime import datetime, timezone
from urllib.parse import urlsplit

@dataclass
class Result:
    url: str
    ok: bool = False
    dns_ms: float = 0
    tcp_ms: float = 0
    tls_ms: float = 0
    http_ms: float = 0
    status: int | None = None
    cert_days: int | None = None
    error: str = ""

async def check(url: str, timeout: float = 5.0) -> Result:
    res = Result(url)
    u = urlsplit(url)
    host, https = u.hostname or "", u.scheme == "https"
    port = u.port or (443 if https else 80)
    loop = asyncio.get_running_loop()
    try:
        async with asyncio.timeout(timeout):
            t0 = time.perf_counter()
            infos = await loop.getaddrinfo(host, port, type=socket.SOCK_STREAM)
            ip = infos[0][4][0]
            t1 = time.perf_counter(); res.dns_ms = (t1 - t0) * 1000

            reader, writer = await asyncio.open_connection(ip, port)
            t2 = time.perf_counter(); res.tcp_ms = (t2 - t1) * 1000

            if https:
                ctx = ssl.create_default_context()
                await writer.start_tls(ctx, server_hostname=host)     # TLS on the open connection
                t3 = time.perf_counter(); res.tls_ms = (t3 - t2) * 1000
                cert = writer.get_extra_info("peercert")
                expires = datetime.fromtimestamp(ssl.cert_time_to_seconds(cert["notAfter"]), timezone.utc)
                res.cert_days = (expires - datetime.now(timezone.utc)).days
            else:
                t3 = t2

            path = u.path or "/"
            writer.write(f"GET {path} HTTP/1.1\r\nHost: {host}\r\nUser-Agent: healthcheck/0.1\r\n"
                         "Connection: close\r\n\r\n".encode())
            await writer.drain()
            status_line = await reader.readline()
            res.http_ms = (time.perf_counter() - t3) * 1000
            res.status = int(status_line.split()[1])
            res.ok = res.status < 500 and (res.cert_days is None or res.cert_days > 14)
            if res.cert_days is not None and res.cert_days <= 14:
                res.error = f"certificate expires in {res.cert_days} days"
            writer.close()
    except TimeoutError:
        res.error = "timeout"
    except (OSError, ssl.SSLError, ValueError, IndexError) as e:
        res.error = f"{type(e).__name__}: {e}"
    return res

async def main(urls: list[str]) -> int:
    results = await asyncio.gather(*(check(u) for u in urls))
    print(f"{'':2} {'URL':<34} {'DNS':>6} {'TCP':>6} {'TLS':>6} {'HTTP':>6} {'CODE':>4} {'CERT':>5}  NOTE")
    for r in results:
        mark = "✅" if r.ok else "❌"
        cert = f"{r.cert_days}d" if r.cert_days is not None else "-"
        print(f"{mark} {r.url:<34} {r.dns_ms:6.0f} {r.tcp_ms:6.0f} {r.tls_ms:6.0f} {r.http_ms:6.0f} "
              f"{r.status or '-':>4} {cert:>5}  {r.error}")
    return 0 if all(r.ok for r in results) else 1

if __name__ == "__main__":
    urls = sys.argv[1:] or ["https://example.com/", "https://www.python.org/", "http://neverssl.com/",
                            "https://expired.badssl.com/", "https://nonexistent.invalid/"]
    raise SystemExit(asyncio.run(main(urls)))
   URL                                   DNS    TCP    TLS   HTTP CODE  CERT  NOTE
✅ https://example.com/                    4     12     25     14  200  60d
✅ https://www.python.org/                 3      9     21     18  200  70d
✅ http://neverssl.com/                    5     90      0     95  200     -
❌ https://expired.badssl.com/             3    160      0      0    -     -  SSLCertVerificationError: ...
❌ https://nonexistent.invalid/            0      0      0      0    -     -  gaierror: ...

The phases match the timeline in the HTTPS lifecycle guide: DNS, then TCP, then TLS, then the HTTP exchange. When one endpoint is slow, the column tells you which layer to debug, the method from Net Ch 34.

Extend it: run in a loop and export Prometheus metrics; add JSON output for a dashboard; store a week of results and flag endpoints whose p95 latency drifts, which is a first step toward the anomaly detection in Part VII.


Generated from real-life-python-guide.md · 8 Oct 2026 16:36