Files
http-latency-prober/docs/plans/2026-07-01-12-23-py-latprobe.md
Jan Novak 24ea9c9e71 feat(py): step 3 — full latprobe package with --verbose flag
Adds the complete Python port of the Go latprobe tool plus diagnostic
verbose output that the Go version does not yet have.

Package (python/latprobe/):
- probe.py: raw-socket HTTP timing with CertInfo/VerboseDetail dataclasses;
  TTFB loop accumulates to \r\n\r\n so headers are parseable without changing
  t_first_byte semantics; captures resolved IP, TLS version/cipher/bits,
  verified cert (via getpeercert()), and all response headers when verbose=True
- aggregate.py: summarize() → per-phase min/avg/max PhaseStats
- cli.py: injectable run(args,stdout,stderr)->int; argparse with injected
  streams; ThreadPoolExecutor concurrency across URLs; four text-rendering
  branches; JSON output; worst-exit-code accumulation; -v/--verbose flag
  appends IP/TLS/cert/header block after every timing table; JSON extended
  with "verbose" object (omitted when flag absent)
- duration.py: parse Go-style duration strings (10s, 500ms, 2m, bare seconds)
- Exit codes: 0 ok, 1 usage, 2 dns, 3 connect, 4 timeout, 5 tls, 6 http≥400

Tests:
- test_probe.py: 18 hermetic tests (success, 404, DNS/connect/timeout/scheme
  failures, partial-phase invariants, verbose detail fields)
- test_cli.py: 38 hermetic tests (usage errors, single/aggregate/failure text,
  worst-code, --fail, JSON schema/ordering/grouping, verbose text and JSON)
- test_integration.py: 45 tests against live internet services (badssl.com for
  TLS errors, real cert/IP/header validation in verbose mode)

Makefile: PYTHONPATH fix, fnmatch pattern test_[!i]*.py to exclude
integration tests from py-test, new py-test-integration target

Docs: docs/usage/py-latprobe.md, python/configs/usage-latprobe.md (runnable
reference with captured output), plans for both the package and verbose feature

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-01 13:45:56 +02:00

4.8 KiB
Raw Blame History

Plan: Full latprobe Python Package

Timestamp: 2026-07-01 12:23 Status: Complete

Goal

Build python/latprobe/ — a packaged Python port of the Go latprobe CLI that mirrors its behavior: flags, sampling, cross-URL concurrency, JSON output, and distinct exit codes.

Package layout

python/
  latprobe/
    __init__.py     — package marker
    __main__.py     — sys.exit(cli.run(sys.argv[1:], sys.stdout, sys.stderr))
    probe.py        — measure(url, opts) -> Result  (raw socket timing)
    aggregate.py    — summarize(results) -> Aggregate  (min/avg/max per phase)
    cli.py          — run(args, stdout, stderr) -> int  (argparse + rendering)
    duration.py     — parse_duration("10s") -> float seconds
  tests/
    test_probe.py   — unittest: success, DNS fail, connect refused, timeout, bad scheme
    test_cli.py     — unittest: drives cli.run() in-process, all branches + JSON

Key design decisions

  • probe.py: reuses the raw-socket technique from phases.py with an Options(timeout) parameter. Partial phases preserved on failure.
  • aggregate.py: summarize(List[Result]) -> Aggregate computes PhaseStats(min_ms, avg_ms, max_ms, present) per phase. Uses only succeeded results.
  • duration.py: parses "10s", "500ms", "2m", bare numbers → float seconds.
  • cli.py: injectable run(args, stdout, stderr) -> int seam for testability.
    • _Parser subclasses argparse.ArgumentParser with custom print_help, print_usage, error, exit to redirect all output to injected streams and raise _ArgExit instead of calling sys.exit.
    • ThreadPoolExecutor for cross-URL concurrency; executor.map preserves input order.
    • Four text-rendering branches: single-success, multi-sample aggregate, all-failed, mixed.
    • Failure summary groups errors by (phase, message) preserving insertion order; N × prefix when grouped.

CLI flags

Flag Default Description
urls (positional, +) required One or more URLs
-n/--count 1 Requests per URL
-c/--concurrency 0 (auto) Max parallel URLs; auto = min(numURLs, 8)
--timeout 10s Per-request timeout (parsed via duration.py)
--fail off Exit non-zero on HTTP status >= 400
--json off Output as JSON array instead of text

Exit codes

Code Meaning
0 All probes succeeded (with --fail: no 4xx/5xx)
1 Usage / argument error
2 DNS failure
3 TCP connect failure
4 Timeout
5 TLS error
6 HTTP status >= 400 (--fail only)

Worst (highest) code across all URLs wins.

JSON schema

[
  {
    "url": "https://example.com",
    "status": 200,
    "succeeded": 3,
    "failed": 2,
    "phases": {
      "dns":      {"min_ms": 1.23, "avg_ms": 1.45, "max_ms": 1.67},
      "connect":  {"min_ms": ...},
      "tls":      {"min_ms": ...},
      "ttfb":     {"min_ms": ...},
      "transfer": {"min_ms": ...},
      "total":    {"min_ms": ...}
    },
    "errors": [
      {"phase": "connect", "count": 2, "message": "..."}
    ]
  }
]
  • phases omitted when succeeded == 0
  • Per-phase key omitted when no successful sample had that phase (e.g. tls for http://)
  • errors omitted when failed == 0

Text output format

Single sample:

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

Aggregate (-n 3):

https://example.com  (200, 3 samples)
                          min        avg        max
  DNS lookup     :    2.36 ms    3.51 ms    5.00 ms
  TCP connect    :   10.25 ms   17.45 ms   31.61 ms
  TLS handshake  :   14.04 ms   41.60 ms   96.50 ms
  Server (TTFB)  :   63.30 ms   66.72 ms   69.78 ms
  Transfer       :    0.26 ms    0.38 ms    0.51 ms
  ─────────────────────────────────────────────────
  Total          :  101.33 ms  139.19 ms  211.99 ms

Testing approach

  • test_probe.py: real http.server.HTTPServer in a daemon thread; closed port for connection-refused; .invalid TLD for DNS failure; black-hole socket (accepts TCP, never sends data) to trigger TTFB timeout.
  • test_cli.py: in-process cli.run(args, io.StringIO(), io.StringIO()) — no subprocess, fast. Covers: usage errors, single/aggregate text, failure exit codes, worst-code accumulation, --fail, JSON schema, JSON ordering, JSON error grouping.

Makefile changes

  • py-test: added PYTHONPATH=$(PY_DIR) so tests can import latprobe; removed || true now that real tests exist.