# `latprobe/` — Full Python Port ## What it does A packaged Python CLI that mirrors the Go `latprobe` tool: per-phase HTTP latency measurement (DNS, TCP connect, TLS, TTFB, Transfer, Total) with configurable sampling, cross-URL concurrency, JSON output, and distinct exit codes per failure class. Run it from the `python/` directory with `python -m latprobe`. Phases measured: | Phase | What is timed | |-------|---------------| | DNS lookup | `socket.getaddrinfo()` — hostname resolution | | TCP connect | `sock.connect()` — SYN to connection established | | TLS handshake | `ssl.wrap_socket()` — full handshake (HTTPS only) | | Server (TTFB) | `sendall()` return → first `recv()` byte | | Transfer | First byte → EOF — body download time | | Total | DNS start → body EOF — wall-clock end-to-end | ## Flags / arguments ``` python -m latprobe [flags] [url ...] ``` | Flag | Default | Description | |------|---------|-------------| | `url …` (positional) | required | One or more URLs to probe | | `-n N`, `--count N` | `1` | Number of requests per URL | | `-c N`, `--concurrency N` | `0` (auto) | Max parallel URLs; `0` = `min(len(urls), 8)` | | `--timeout DURATION` | `10s` | Per-request timeout; supports `ms`, `s`, `m`, or bare seconds | | `--fail` | off | Exit non-zero when any HTTP status ≥ 400 | | `--json` | off | Output as JSON array instead of text | | `-v`, `--verbose` | off | Show resolved IP, TLS version/cipher, certificate details, and response headers | | `-h`, `--help` | — | Show help and exit 0 | **Exit codes:** | Code | Meaning | |------|---------| | 0 | All probes succeeded | | 1 | Usage / argument error | | 2 | DNS failure | | 3 | TCP connect failure | | 4 | Timeout | | 5 | TLS error | | 6 | HTTP status ≥ 400 (`--fail` only) | The highest exit code across all URLs is used as the process exit. ## Examples ### Single URL ```sh python -m latprobe https://example.com ``` ``` https://example.com (200) DNS lookup : 18.87 ms TCP connect : 9.76 ms TLS handshake : 14.71 ms Server (TTFB) : 67.07 ms Transfer : 0.14 ms ───────────────────────────── Total : 118.39 ms ``` ### Sampling (`-n`) — shows min/avg/max table ```sh python -m latprobe -n 5 https://example.com ``` ``` https://example.com (200, 5 samples) min avg max DNS lookup : 1.80 ms 3.14 ms 5.00 ms TCP connect : 9.50 ms 10.25 ms 11.40 ms TLS handshake : 13.80 ms 15.20 ms 18.90 ms Server (TTFB) : 62.00 ms 66.50 ms 71.30 ms Transfer : 0.10 ms 0.25 ms 0.40 ms ───────────────────────────────────────────────── Total : 97.10 ms 101.20 ms 109.80 ms ``` ### Multiple URLs (probed in parallel) ```sh python -m latprobe https://example.com https://www.google.com ``` Output for each URL is separated by a blank line. Exit code = worst across all. ### JSON output ```sh python -m latprobe --json -n 3 https://example.com ``` ```json [ { "url": "https://example.com", "status": 200, "succeeded": 3, "failed": 0, "phases": { "dns": {"min_ms": 1.80, "avg_ms": 3.14, "max_ms": 5.00}, "connect": {"min_ms": 9.50, "avg_ms": 10.25, "max_ms": 11.40}, "tls": {"min_ms": 13.80, "avg_ms": 15.20, "max_ms": 18.90}, "ttfb": {"min_ms": 62.00, "avg_ms": 66.50, "max_ms": 71.30}, "transfer": {"min_ms": 0.10, "avg_ms": 0.25, "max_ms": 0.40}, "total": {"min_ms": 97.10, "avg_ms": 101.20, "max_ms": 109.80} } } ] ``` `phases` is omitted when all samples failed. `tls` key is omitted for `http://` URLs. `errors` is included (and `phases` omitted) when some samples fail. ### Verbose mode (`-v` / `--verbose`) Shows the resolved IP address, TLS version/cipher/bits, certificate details (CN, expiry, issuer), and useful response headers. Appended to the standard timing block. ```sh python -m latprobe --verbose https://example.com ``` ``` https://example.com (200) DNS lookup : 16.47 ms TCP connect : 9.76 ms TLS handshake : 13.09 ms Server (TTFB) : 60.31 ms Transfer : 0.20 ms ───────────────────────────── Total : 112.76 ms IP : 104.20.23.154 TLS : TLSv1.3 TLS_AES_256_GCM_SHA384 256 bit Cert : CN=example.com valid until 2026-08-29 SSL Corporation Server : cloudflare Content-Type : text/html ``` For **TLS failures**, the verbose block still shows the IP (DNS + TCP succeeded) so you can tell which server you actually reached: ```sh python -m latprobe --verbose https://expired.badssl.com/ ``` ``` https://expired.badssl.com/ (FAILED) DNS lookup : 30.98 ms TCP connect : 126.58 ms TLS handshake : 293.58 ms ───────────────────────────── Total : 463.37 ms ✗ tls: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: certificate has expired IP : 104.154.89.105 ``` For **DNS failures**, the verbose block is empty (IP unknown), so it is not printed. Combined with `--json`, verbose detail appears in a `"verbose"` object: ```sh python -m latprobe --verbose --json https://example.com ``` ```json [ { "url": "https://example.com", "status": 200, "succeeded": 1, "failed": 0, "phases": { ... }, "verbose": { "ip": "104.20.23.154", "tls_version": "TLSv1.3", "tls_cipher": "TLS_AES_256_GCM_SHA384", "tls_bits": 256, "cert": { "cn": "example.com", "sans": ["example.com", "*.example.com"], "expiry": "2026-08-29", "issuer_cn": "SSL Corporation", "verified": true }, "headers": { "Content-Type": "text/html", "Server": "cloudflare", ... } } } ] ``` The `"verbose"` key is omitted when `--verbose` is not set. The `"cert"` key is omitted for `http://` URLs and when TLS fails (Python's stdlib does not expose parsed cert data from a failed handshake). `"headers"` contains **all** parsed response headers (the text block shows a curated priority list). ### DNS failure ```sh python -m latprobe http://no.such.host.invalid echo "exit: $?" ``` ``` http://no.such.host.invalid (FAILED) Total : 0.65 ms ✗ dns: [Errno 8] nodename nor servname provided, or not known exit: 2 ``` ### `--fail` flag (exit non-zero on HTTP 4xx/5xx) ```sh python -m latprobe --fail https://www.google.com/this-page-does-not-exist-at-all echo "exit: $?" ``` ``` https://www.google.com/this-page-does-not-exist-at-all (404 ✗) DNS lookup : 3.10 ms TCP connect : 9.60 ms TLS handshake : 14.20 ms Server (TTFB) : 118.50 ms Transfer : 0.12 ms ───────────────────────────── Total : 145.50 ms exit: 6 ``` ### Mixed — some URLs succeed, some fail ```sh python -m latprobe https://example.com http://no.such.host.invalid echo "exit: $?" ``` ``` https://example.com (200) ... Total : 118.39 ms http://no.such.host.invalid (FAILED) Total : 0.65 ms ✗ dns: ... exit: 2 ``` ### Timeout ```sh python -m latprobe --timeout 500ms http://10.255.255.1/ echo "exit: $?" ``` ``` http://10.255.255.1/ (FAILED) DNS lookup : 0.20 ms TCP connect : 500.18 ms ───────────────────────────── Total : 500.40 ms ✗ timeout: timed out exit: 4 ``` ## Makefile targets ```sh make py-run ARGS="-n 3 https://example.com" # run the package make py-test # run test suite make py-check # alias for py-test ``` ## Limitations - **DNS timeout is OS-controlled.** Python's `socket.getaddrinfo` does not accept a timeout parameter. The `--timeout` flag applies to TCP connect and subsequent phases. DNS failures from an unreachable or NXDOMAIN host still happen quickly in practice. - **HTTP 1.1 + `Connection: close` only.** No keep-alive, HTTP/2, auth, custom headers, or redirect following. HTTP 3xx is shown with its raw status code. - **Body fully drained.** Transfer time is real download time; large bodies affect the Transfer and Total phases. ## Comparison with `simple.py` and `phases.py` | | `simple.py` | `phases.py` | `latprobe/` | |--|-------------|-------------|-------------| | Phases | total only | all phases | all phases | | Sampling | no | no | `-n` flag | | Concurrency | no | no | `-c` flag | | JSON | no | no | `--json` flag | | `--fail` | implicit (urlopen raises on 4xx) | no | `--fail` flag | | Exit codes | 0 or 1 | 0 or 1 | 0–6 per failure class | | Config file | yes | yes | no (URL args only) |