Plans (docs/plans/): - 2026-07-01-23-47-py-hxprobe-httpx.md — initial httpx probe design - 2026-07-02-09-32 through 14-05 — standalone project, toolchain, usage doc + Makefile, file input (-f), simplification pass, run-summary footer Summaries (docs/summaries/): one per completed feature, recording what was actually built, deviations from the plan, and verification steps Explanations (docs/explanations/): two deep-dives written during review — hxprobe concurrency model and worst-exit-code + render-loop analysis Usage (docs/usage/hxprobe.md): overview with pointer to hxprobe/USAGE.md for the full runnable reference Walkthrough (docs/py-latprobe-walkthrough.md): narrative tour of the latprobe Python package for interview / code-review context CHANGELOG.md: entries for all hxprobe features (toolchain, usage doc, file input, simplification, run-summary footer) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
434 lines
24 KiB
Markdown
434 lines
24 KiB
Markdown
# Changelog
|
||
|
||
All completed features are logged here in reverse-chronological order.
|
||
|
||
---
|
||
|
||
## 2026-07-02 14:05 — `hxprobe`: end-of-run summary footer for multi-URL runs
|
||
|
||
- `hxprobe/hxprobe/cli.py`: when more than one URL is probed (positional args
|
||
or `-f`), a footer is now appended after the last URL block — a tally of
|
||
every URL's outcome (`N ok`, `M failed` broken down by class) plus the
|
||
`→ exit N (label)` line tying it to the process exit code. Single-URL text
|
||
output and JSON output (still a bare array) are both byte-identical to
|
||
before — new `_print_run_summary()`, gated on `len(urls) > 1` and text mode
|
||
only
|
||
- The process **exit code itself is unchanged**: still the highest severity
|
||
across all URLs (worst-code wins), matching `latprobe`/Go and the 13
|
||
existing exit-code tests — a deliberate decision, since that scalar is a
|
||
documented cross-implementation contract (`hxprobe/USAGE.md`'s Exit codes
|
||
table). The footer exists to give humans the per-URL breakdown the scalar
|
||
can't show, not to change what gets returned
|
||
- New `_EXIT_LABELS` (inverse of `_PHASE_EXIT`) for rendering exit codes as
|
||
short labels (`dns`, `tls`, `http`, …) in the footer
|
||
- Refactored the per-URL accumulation loop to compute one worst-code per URL
|
||
first, then fold into the global `worst` — this also unified the two
|
||
"running max" idioms (`if c > worst: worst = c` vs `max(worst, ...)`) that
|
||
`docs/explanations/2026-07-02-13-25-hxprobe-worst-exit-code-and-render-loop.md`
|
||
had flagged as a stylistic wrinkle into a single `max(...)` call
|
||
- `hxprobe/tests/test_cli.py`: 3 new hermetic tests (`TestCLIRunSummary`) —
|
||
multi-URL mixed outcome, multi-URL all-ok, and single-URL-has-no-footer;
|
||
all 33 pre-existing tests pass unedited
|
||
- Triggered by a follow-up question: does a single worst-code exit even make
|
||
sense across multiple URLs with different error classes? Answer: keep the
|
||
scalar (parity contract) but add the missing visibility as output, not by
|
||
changing the exit code's semantics
|
||
- `hxprobe/USAGE.md`: new "Multi-URL summary footer" section with real
|
||
captured output; refreshed the "Multiple URLs" and `-f` multi-URL examples
|
||
to show the footer (previously stale — captured before this feature
|
||
existed); added a sentence to the Exit codes section
|
||
- `docs/usage/hxprobe.md`: one-line mention pointing at the new section
|
||
- Plan: `docs/plans/2026-07-02-14-05-hxprobe-run-summary-footer.md`
|
||
- Summary: `docs/summaries/2026-07-02-14-05-hxprobe-run-summary-footer.md`
|
||
|
||
---
|
||
|
||
## 2026-07-02 12:05 — `hxprobe` simplification pass (no behavior change)
|
||
|
||
- `hxprobe/hxprobe/cli.py`: deleted the `_Parser`/`_ArgExit` scaffolding
|
||
(~32 lines) that hand-reimplemented what `argparse.ArgumentParser` already
|
||
does (help→stdout, usage/errors→stderr, raise instead of hard-exit);
|
||
replaced with a plain `ArgumentParser` wrapped in
|
||
`contextlib.redirect_stdout`/`redirect_stderr`, catching `SystemExit` at one
|
||
site
|
||
- `hxprobe/hxprobe/probe.py`: trimmed `_TimingStream.get_extra_info` to the
|
||
one branch (`ssl_object`) actually consumed on hxprobe's request path — the
|
||
`server_addr`/`client_addr` branches were dead (only `httpx`'s own CLI
|
||
queries them); removed `_dns_set`/`_connect_set`/`_tls_set` from `_Trace`,
|
||
redundant with the `Phase.present` flag already on `self.dns`/`connect`/`tls`
|
||
- `hxprobe/hxprobe/cli.py`: `_load_urls` no longer takes only the first
|
||
whitespace token per line ("forward-compatible with future `key=value`
|
||
annotations" that never materialized); extracted `_summarize_failures()`
|
||
to remove a duplicated dedup loop shared by `_print_failure_summary` and
|
||
`_build_json_entry`
|
||
- Triggered by a question about a dead `file=` parameter on
|
||
`_Parser.print_help`/`print_usage`; verified no behavior changed — all 33
|
||
existing tests pass unedited, plus manual smoke tests of `-h`, no-args
|
||
(stdout/stderr routing double-checked via separate file redirection), and a
|
||
live verbose request
|
||
- Plan: `docs/plans/2026-07-02-12-05-hxprobe-simplification.md`
|
||
- Summary: `docs/summaries/2026-07-02-12-05-hxprobe-simplification.md`
|
||
|
||
---
|
||
|
||
## 2026-07-02 11:14 — `hxprobe`: read target URLs from a file (`-f`/`--file`)
|
||
|
||
- `hxprobe/hxprobe/cli.py`: new `-f`/`--file PATH` flag reads URLs from a
|
||
plain-text file (one per line, `#` comments, blank lines skipped, first
|
||
whitespace-separated token per line) — same format as `simple.py`'s
|
||
`load_sites()`, reimplemented locally as `_load_urls()` rather than
|
||
imported (hxprobe still imports nothing outside its own directory)
|
||
- `-f`/`--file` is **mutually exclusive** with positional `url` args (both
|
||
or neither given → usage error); `urls` positional changed from
|
||
`nargs="+"` to `nargs="*"` to allow the file-only case
|
||
- Missing file / unreadable file / empty file all produce a clear usage
|
||
error (`EXIT_USAGE`) rather than a traceback
|
||
- `hxprobe/configs/*.txt`: 7 new fixtures mirroring `python/configs/`'s set
|
||
(all-ok, dns-failure, connection-refused, timeout, tls-errors,
|
||
http-errors, mixed), each with a verified expected exit code in its
|
||
header comment — actually run against `hxprobe -f ...` during
|
||
implementation, not assumed from the `simple.py` originals (whose
|
||
blanket 0/1 exit scheme differs from hxprobe's per-failure-class 0–6)
|
||
- `hxprobe/tests/test_cli.py`: 5 new hermetic tests (`TestCLIFileInput`) —
|
||
successful multi-URL read from a temp file, missing file, empty file,
|
||
both-sources-given, and neither-given error paths
|
||
- `hxprobe/USAGE.md`: new "Reading URLs from a file (`-f`)" section with
|
||
real captured output, placed after "Multiple URLs"
|
||
- Plan: `docs/plans/2026-07-02-11-14-hxprobe-file-input.md`
|
||
- Summary: `docs/summaries/2026-07-02-11-14-hxprobe-file-input.md`
|
||
|
||
---
|
||
|
||
## 2026-07-02 10:22 — `hxprobe` usage reference doc + standalone Makefile
|
||
|
||
- `hxprobe/USAGE.md`: new "Runnable Usage Reference" matching the depth of
|
||
`python/configs/usage-latprobe.md` — 16 cases with real captured output
|
||
(basic, verbose × HTTPS/plain-HTTP/redirect-followed/`--no-follow-
|
||
redirects`/`--no-http2`/TLS-failure/DNS-failure, sampling, multi-URL,
|
||
`--fail`, JSON, JSON+verbose, timeout, exit-codes table, Makefile
|
||
shortcuts). Placed inside `hxprobe/` itself (not `python/configs/`) so it
|
||
travels with the project if extracted to its own repo
|
||
- Timeout example uses `192.0.2.1` (RFC 5737 TEST-NET-1) instead of
|
||
`10.255.255.1` — the latter resolves to an immediate "connection refused"
|
||
in this dev sandbox rather than a genuine timeout; `192.0.2.1` reproduces
|
||
a real ~500ms timeout reliably
|
||
- `docs/usage/hxprobe.md`: added a one-line pointer to `hxprobe/USAGE.md` at
|
||
the top; no other changes — it stays the CLAUDE.md-mandated summary doc
|
||
- `hxprobe/Makefile`: new, fully standalone (`help`, `deps`, `run`, `lint`,
|
||
`fmt`, `test`, `test-integration`, `check`, `clean`) — same auto-generated
|
||
`## comment` help style as the root Makefile, short target names (no
|
||
`hx-` prefix needed inside `hxprobe/`'s own scope). Deliberately
|
||
independent from the root Makefile's `hx-*` targets — neither calls into
|
||
the other, per explicit user decision (avoids one Makefile's changes
|
||
silently breaking the other, at the cost of some duplicated `uv`/`pytest`
|
||
invocation logic)
|
||
- No changes to the root `Makefile` — verified its `hx-*` section is
|
||
byte-for-byte unchanged
|
||
- Plan: `docs/plans/2026-07-02-10-22-hxprobe-usage-doc-and-makefile.md`
|
||
- Summary: `docs/summaries/2026-07-02-10-22-hxprobe-usage-doc-and-makefile.md`
|
||
|
||
---
|
||
|
||
## 2026-07-02 09:57 — `hxprobe` toolchain modernization (uv, ruff, pytest)
|
||
|
||
- Adopted `uv` for environment/dependency management: `hxprobe/pyproject.toml`
|
||
gets a `[dependency-groups] dev = ["pytest>=8.0", "ruff>=0.8"]` section;
|
||
`hxprobe/.python-version` pins Python 3.14; `hxprobe/uv.lock` (new, 18
|
||
packages resolved) pins every transitive dependency (`httpx`, `httpcore`,
|
||
`h2`, `certifi`, etc.) for reproducible installs — previously nothing was
|
||
pinned beyond `httpx[http2]>=0.28`
|
||
- Added `ruff` for linting + formatting: `[tool.ruff]` config
|
||
(`target-version = "py311"`, `line-length = 100`); fixed the one real
|
||
finding (`import sys` unused in `cli.py`, inherited from the original
|
||
`latprobe/cli.py`); ran `ruff format` across the project (6 files
|
||
reformatted — mostly collapsing the hand-aligned `=`/dict-key columns to
|
||
single-space, no semantic changes; verified via full syntax check + test
|
||
run before and after)
|
||
- Swapped the test runner from stdlib `unittest discover` to `pytest`.
|
||
Existing `unittest.TestCase` classes run unchanged (pytest is a superset
|
||
runner) — no test-code rewrite. Replaced the `test_[!i]*.py` filename-glob
|
||
hermetic/integration split with a proper `pytest.mark.integration` marker
|
||
(`pytestmark = pytest.mark.integration` in `tests/test_integration.py`);
|
||
registered in `[tool.pytest.ini_options]` to avoid unknown-marker warnings
|
||
- Skipped mypy for now (type hints stay as documentation only) — explicit
|
||
user decision, not an oversight
|
||
- `hxprobe/README.md`: new — quick start (`uv sync`, `uv run ...`) for the
|
||
standalone project, independent of the parent repo's docs
|
||
- Makefile: `hx-deps`/`hx-run`/`hx-test`/`hx-test-integration` now shell out
|
||
to `uv sync`/`uv run` instead of hand-managed `python -m venv` + `pip
|
||
install -e .` (removed `HX_VENV`/`HX_VENV_PYTHON` vars — uv owns this now);
|
||
added `hx-lint`/`hx-fmt`; `hx-check` now runs lint + hermetic tests
|
||
(mirrors `go-check`'s fmt+vet+test bundling, previously only ran tests)
|
||
- `docs/usage/hxprobe.md`: updated Setup and Makefile-targets sections for
|
||
the new toolchain
|
||
- `.gitignore`: added `.pytest_cache/`/`.ruff_cache/`
|
||
- No behavior change to the probe/CLI itself — confirmed identical output
|
||
before/after, `python/latprobe` untouched, zero `latprobe` imports remain
|
||
in `hxprobe/`
|
||
- Plan: `docs/plans/2026-07-02-09-57-hxprobe-toolchain-modernization.md`
|
||
- Summary: `docs/summaries/2026-07-02-09-57-hxprobe-toolchain-modernization.md`
|
||
|
||
---
|
||
|
||
## 2026-07-02 09:32 — `hxprobe` extracted into a standalone top-level project
|
||
|
||
- Moved `hxprobe` out of `python/` into a new top-level `hxprobe/` directory
|
||
(sibling of `go/` and `python/`), with its own `pyproject.toml`, its own
|
||
venv (`hxprobe/.venv`), and its own `hxprobe/tests/` — structured so it
|
||
could be `cp -r`'d into a separate repo and work unchanged
|
||
- Removed every `from latprobe import ...` in `hxprobe/`: `probe.py` now
|
||
defines its own `Options`/`Phase`/`Result`/`VerboseDetail`/`CertInfo`
|
||
dataclasses and its own `_parse_cert`/`_parse_cert_date` helpers (copied,
|
||
not shared); `aggregate.py`/`duration.py` are verbatim copies (their
|
||
imports were already package-relative, so no edits needed);
|
||
`cli.py` is now a full standalone implementation (own exit codes, argparse,
|
||
text/JSON rendering) instead of delegating to `latprobe.cli.run()` via an
|
||
injected `measure_fn`
|
||
- Reverted `python/latprobe/{cli.py,probe.py}` to their pre-`hxprobe` state
|
||
(`git checkout --`) — the `measure_fn`/`prog`/`description`/`protocol_flags`
|
||
injection points, `Options.follow_redirects`/`http2`, and
|
||
`VerboseDetail.http_version`/`redirect_count` only existed to support the
|
||
now-removed sharing; `python/` is back to zero third-party dependencies, no
|
||
`pyproject.toml`, no venv
|
||
- Makefile: removed `py-deps` and the `PY_VENV*` variables; `py-test`/
|
||
`py-check` reverted to running directly against `$(PYTHON)`; added a new
|
||
standalone `hx-deps`/`hx-run`/`hx-test`/`hx-test-integration`/`hx-check`/
|
||
`hx-clean` section using `HX_DIR`/`HX_VENV*`; umbrella `test`/`check`/
|
||
`clean` now include the hxprobe targets alongside Go and Python
|
||
(`go-*`/`py-*`/`hx-*`)
|
||
- Tests moved and renamed to drop the now-redundant `hx_`/`_hx` segments:
|
||
`test_hx_probe.py` → `hxprobe/tests/test_probe.py`,
|
||
`test_hx_cli.py` → `hxprobe/tests/test_cli.py`,
|
||
`test_integration_hx.py` → `hxprobe/tests/test_integration.py` (same
|
||
`test_[!i]*.py` hermetic-vs-integration exclusion convention as `latprobe`)
|
||
- `docs/usage/py-hxprobe.md` renamed to `docs/usage/hxprobe.md` and rewritten
|
||
for the new standalone structure and Makefile targets; the TCP_NODELAY/
|
||
Nagle TTFB-accuracy finding is preserved
|
||
- No behavior change — same CLI flags, same output, same six-phase timing,
|
||
same HTTP/2/redirect handling as before; this was a pure decoupling/
|
||
restructuring
|
||
- Plan: `docs/plans/2026-07-02-09-32-hxprobe-standalone-project.md`
|
||
- Summary: `docs/summaries/2026-07-02-09-32-hxprobe-standalone-project.md`
|
||
|
||
---
|
||
|
||
## 2026-07-02 00:24 — `hxprobe`: httpx-based Python probe, Go-client parity
|
||
|
||
- New sibling package `python/hxprobe/`, built on `httpx` instead of raw
|
||
sockets, so the client matches Go's `http.DefaultClient`: HTTP/2 negotiated
|
||
via ALPN, redirects followed by default, connection pooling, default TLS
|
||
verification — while still reporting the full six-phase breakdown (DNS, TCP
|
||
connect, TLS, TTFB, Transfer, Total)
|
||
- `hxprobe/probe.py`: DNS/TCP connect/TLS timed by instrumenting a custom
|
||
httpcore `NetworkBackend`/`NetworkStream` (`_TimingBackend`/`_TimingStream`);
|
||
HTTP framing (HTTP/1.1 or HTTP/2), redirects, and keep-alive stay entirely
|
||
owned by httpx; `measure()` returns the same `latprobe.probe.Result`
|
||
dataclass, so aggregation/rendering are reused unchanged
|
||
- `latprobe/probe.py`: added `Options.follow_redirects`/`Options.http2`
|
||
(ignored by the raw-socket `measure()`) and
|
||
`VerboseDetail.http_version`/`redirect_count` (always `""`/`0` there)
|
||
- `latprobe/cli.py`: `run()`/`_run_samples()` take an injectable `measure_fn`
|
||
(default: the existing socket `measure`), plus `prog`/`description`/
|
||
`protocol_flags` overrides — `hxprobe.cli.run()` reuses the entire argparse,
|
||
concurrency, exit-code, and text/JSON rendering pipeline unchanged
|
||
- New CLI flags (hxprobe only, via `protocol_flags=True`): `--no-http2`,
|
||
`--no-follow-redirects`
|
||
- `python/pyproject.toml`: first third-party dependency in this repo
|
||
(`httpx[http2]`); `make py-deps` provisions `python/.venv`
|
||
- Verified experimentally that `latprobe`'s raw socket — which never sets
|
||
`TCP_NODELAY` — pays a real ~40-50ms Nagle/delayed-ACK penalty on its TTFB
|
||
phase; `hxprobe` sets `TCP_NODELAY` (matching httpcore's default and Go's
|
||
`net.Dialer`) and does not. Documented in `docs/usage/py-hxprobe.md` as a
|
||
known divergence — `hxprobe`'s TTFB is the more accurate of the two, not
|
||
just different
|
||
- 17 new hermetic tests (`tests/test_hx_probe.py`), 11 new hermetic CLI tests
|
||
(`tests/test_hx_cli.py`), 9 new live integration tests
|
||
(`tests/test_integration_hx.py`, excluded from the default gate)
|
||
- Makefile: `py-deps`, `hx-run`, `hx-test-integration`; `py-test`/`py-check`
|
||
now run via the venv and include the new hermetic hxprobe tests
|
||
- User doc: `docs/usage/py-hxprobe.md`
|
||
- Plan: `docs/plans/2026-07-01-23-47-py-hxprobe-httpx.md`
|
||
|
||
---
|
||
|
||
## 2026-07-01 12:55 — `--verbose` / `-v` flag for `latprobe` (Python)
|
||
|
||
- `-v`/`--verbose` flag added to the `latprobe` CLI
|
||
- `probe.py`: new `CertInfo` + `VerboseDetail` dataclasses; `Options.verbose`;
|
||
`Result.detail`; TTFB loop now accumulates until `\r\n\r\n` (unchanged TTFB
|
||
semantics — `t_first_byte` stamped on first `recv()`, not when headers complete);
|
||
captures resolved IP (from `getaddrinfo`), TLS version/cipher/bits
|
||
(from `sock.version()`/`sock.cipher()`), verified cert (from `getpeercert()`),
|
||
and all response headers
|
||
- `cli.py`: verbose text block appended after timing rows in all four output
|
||
branches (single, aggregate, all-failed, mixed); `_VERBOSE_HEADERS` priority
|
||
list controls which headers appear in text mode; JSON `"verbose"` object
|
||
includes all parsed headers, full cert fields, TLS metadata; `"verbose"` key
|
||
omitted when flag is absent
|
||
- 9 new hermetic probe tests, 15 new CLI tests covering verbose text and JSON
|
||
across success, connect-fail, and DNS-fail scenarios
|
||
- 15 new integration tests for real TLS cert fields (CN, expiry, issuer),
|
||
IP format, TLS version string, header presence, and verbose text/JSON output
|
||
- `python/configs/usage-latprobe.md`: runnable reference with real output for
|
||
all features including verbose and `--verbose --json`
|
||
- `docs/usage/py-latprobe.md`: updated with `--verbose` flag and examples
|
||
- Plan: `docs/plans/2026-07-01-12-55-py-latprobe-verbose.md`
|
||
|
||
---
|
||
|
||
## 2026-07-01 12:23 — Full `latprobe` Python package (Python, Step 3)
|
||
|
||
- `python/latprobe/` package: full port of the Go CLI, runnable as `python -m latprobe`
|
||
- `probe.py`: raw-socket HTTP measurement with `Options(timeout)` parameter; mirrors
|
||
`phases.py` technique; partial phases preserved on failure
|
||
- `aggregate.py`: `summarize(List[Result]) -> Aggregate` with per-phase `PhaseStats(min_ms, avg_ms, max_ms)`
|
||
- `duration.py`: parses Go-style duration strings (`10s`, `500ms`, `2m`, bare seconds)
|
||
- `cli.py`: injectable `run(args, stdout, stderr) -> int`; argparse with injected streams
|
||
(`_Parser` subclass); `ThreadPoolExecutor` concurrency across URLs; four text-rendering
|
||
branches (single, aggregate, all-failed, mixed); JSON output; worst-exit-code accumulation
|
||
- Exit codes: 0 ok, 1 usage, 2 dns, 3 connect, 4 timeout, 5 tls, 6 http≥400 (`--fail`)
|
||
- `python/tests/test_probe.py`: 9 tests — success, 404, DNS fail, connect refused, TTFB
|
||
timeout, bad scheme, partial phase invariants; uses `http.server` + daemon threads
|
||
- `python/tests/test_cli.py`: 23 tests — drives `cli.run()` in-process; covers usage
|
||
errors, single/aggregate text, failure exit codes, worst-code, `--fail`, JSON schema,
|
||
JSON ordering, JSON error grouping
|
||
- Makefile `py-test`: added `PYTHONPATH=$(PY_DIR)`, removed `|| true`
|
||
- User doc: `docs/usage/py-latprobe.md`
|
||
|
||
---
|
||
|
||
## 2026-07-01 12:04 — Per-phase latency measurement (Python, Step 2)
|
||
|
||
- `python/phases.py`: self-contained script; hand-drives raw sockets to time
|
||
each HTTP phase individually — DNS (`getaddrinfo`), TCP connect, TLS handshake
|
||
(`ssl.wrap_socket`, HTTPS only), TTFB (sendall → first recv), Transfer, Total
|
||
- Partial phases preserved on failure (same invariant as Go's `probe.go`)
|
||
- Error classification mirrors Go's priority: dns → timeout → tls → connect
|
||
- Output format matches Go's single-sample text layout (14-char labels, `─`
|
||
separator, `%8.2f ms` alignment)
|
||
- Input: bare URL args or plain-text config file (same format as `simple.py`)
|
||
- Exits 0 if all URLs complete without network error; 1 if any failed
|
||
- User doc: `docs/usage/py-phases.md`
|
||
|
||
---
|
||
|
||
## 2026-07-01 11:30 — Error-path example configs for simple.py (Python, Step 1 refinement)
|
||
|
||
- `python/configs/` directory with 7 purpose-built config files, one per error class:
|
||
`all-ok.txt`, `dns-failure.txt`, `connection-refused.txt`, `timeout.txt`,
|
||
`tls-errors.txt` (badssl.com), `http-errors.txt` (httpstat.us), `mixed.txt`
|
||
- `python/configs/usage.md`: runnable shell commands + expected output for every config
|
||
- Fixed `docs/usage/py-simple.md`: 4xx/5xx responses are reported as `FAIL` (not `OK`),
|
||
because `urlopen` raises `HTTPError` for non-2xx; added pointer to the example configs
|
||
|
||
---
|
||
|
||
## 2026-07-01 10:39 — Simple reachability checker (Python, Step 1)
|
||
|
||
- `python/simple.py`: reads a plain-text site list (one URL per line, `#` comments),
|
||
issues a GET to each, prints aligned `OK / FAIL + elapsed ms` per site
|
||
- Plain-text config format forward-compatible with future `key=value` annotations
|
||
- Exits 0 if all sites responded, 1 if any failed or config is missing
|
||
- `python/sites.txt`: committed example config
|
||
- Makefile `py-*` targets added: `py-simple-run`, `py-phases-run`, `py-run`,
|
||
`py-test`, `py-check`, `py-clean`; umbrella `test`, `check`, `clean` now include Python
|
||
- User doc: `docs/usage/py-simple.md`
|
||
|
||
---
|
||
|
||
## 2026-07-01 01:23 — Concurrency (Go, Step 7)
|
||
|
||
- Added `-c`/`--concurrency` flag: max URLs probed in parallel (0 = auto)
|
||
- Default auto-concurrency: `min(numURLs, 8)` — scales with workload, caps at 8
|
||
- Parallelism is **across URLs only**; the N samples of each URL stay sequential
|
||
to preserve the accuracy of per-URL min/avg/max statistics
|
||
- Output buffered and printed in original input order (deterministic for both
|
||
text and JSON), exit-code accumulation unchanged
|
||
- Implementation: semaphore channel + `sync.WaitGroup`, each goroutine writes
|
||
only its own indexed result slot — verified clean with `go test -race`
|
||
- Added `make go-test-race` target to the Makefile
|
||
- 3 new tests: `TestRunConcurrentOrder`, `TestConcurrentWorstCode`,
|
||
`TestConcurrentJSONOrder`
|
||
- Updated runtime estimates in `docs/custom-usage-examples.md`
|
||
- User doc: `docs/usage/step-7-concurrency.md`
|
||
|
||
---
|
||
|
||
## 2026-07-01 01:05 — Root Makefile
|
||
|
||
- Namespaced targets: `go-build`, `go-run`, `go-test`, `go-test-verbose`, `go-check`, `go-cover`, `go-fmt`, `go-vet`, `go-tidy`, `go-install`, `go-lint`, `go-clean`
|
||
- Umbrella targets (`build`, `test`, `check`, `fmt`, `vet`, `clean`, `all`) delegate to Go now; `py-*` will slot in during the Python port
|
||
- `help` is the default target; auto-generated from `##` comments
|
||
- `go-run` accepts `ARGS=` for passing flags, `go-lint` guards for `golangci-lint`
|
||
- `.gitignore` updated with coverage artifacts
|
||
|
||
---
|
||
|
||
## 2026-07-01 00:49 — Integration tests (Go, Step 6)
|
||
|
||
- Extracted `run(args, stdout, stderr) int` from `main()` to make the CLI testable in-process
|
||
- Fixed TLS classification bug: `TLSHandshakeDone` fires with the error on cert rejection, so `tlsErr` is now captured and checked before the stale `tlsStart.IsZero()` guard
|
||
- `run_test.go`: 14 in-process tests covering exit codes 0–6, multi-URL, sampling, and JSON structure assertions
|
||
- `cli_test.go`: `TestMain` builds the real binary; 3 subprocess smoke tests exercise the actual `os.Exit` path
|
||
- All test servers use `httptest` + stdlib; `.invalid` TLD for deterministic DNS failures; no external network dependency
|
||
|
||
---
|
||
|
||
## 2026-07-01 00:38 — Failure handling (Go, Step 5)
|
||
|
||
- Classified network failures into `dns` / `connect` / `timeout` / `tls` with distinct exit codes (2–5)
|
||
- Partial timing preserved up to the failure point (e.g. DNS phase shown on NXDOMAIN)
|
||
- `--timeout` flag (default 10s) applied via `context.WithTimeout`
|
||
- `--fail` flag: HTTP status ≥ 400 → exit code 6 (curl-style)
|
||
- `-n` sampling continues on network failure; aggregates successes, reports fail count + cause
|
||
- JSON output extended with `succeeded`, `failed`, `errors[]` fields
|
||
- Highest exit code across all URLs/failure types is used as the process exit
|
||
|
||
---
|
||
|
||
## 2026-07-01 00:08 — JSON output flag (Go, Step 4)
|
||
|
||
- `--json` flag emits a JSON array with one entry per URL
|
||
- Schema always uses min/avg/max shape (consistent regardless of `-n`)
|
||
- Phases absent from the request (e.g. TLS on HTTP) are omitted from the JSON object
|
||
- Failed URLs are excluded from JSON output and reported to stderr
|
||
- Text output unchanged and remains the default
|
||
|
||
---
|
||
|
||
## 2026-07-01 00:08 — Multiple URLs + sampling (Go, Step 3)
|
||
|
||
- `-n`/`--count` flag repeats each URL N times and reports min/avg/max per phase
|
||
- `probe.Summarize` aggregates a slice of Results into per-phase PhaseStats
|
||
- Multi-sample output shows an aligned min/avg/max table with a header row
|
||
- Single-sample output (n=1) is unchanged from Step 2
|
||
|
||
---
|
||
|
||
## 2026-07-01 00:08 — Per-phase breakdown (Go, Step 2)
|
||
|
||
- Instrumented requests with `net/http/httptrace.ClientTrace`
|
||
- DNS lookup, TCP connect, TLS handshake, Server/TTFB, Transfer, Total phases
|
||
- TLS row omitted automatically for plain `http://` URLs
|
||
- Aligned text output with separator before Total
|
||
|
||
---
|
||
|
||
## 2026-07-01 00:08 — Simple total latency (Go, Step 1)
|
||
|
||
- `probe.Measure` performs an HTTP GET and records wall-clock total time
|
||
- `main.go` prints URL, HTTP status code, and total duration for each URL
|
||
- Exits with code 1 if any URL fails
|
||
- Multiple URLs accepted as positional arguments
|
||
|
||
---
|
||
|
||
## 2026-07-01 00:08 — Project scaffold (Go, Step 0)
|
||
|
||
- Created project structure: `go/`, `python/`, `docs/plans/`, `docs/usage/`
|
||
- Initialised Go module `latprobe` (`go/go.mod`)
|
||
- Minimal `go/main.go` that prints usage and exits cleanly when no URL is given
|
||
- Stub `go/internal/probe/probe.go` defining the `Result` type and `Measure` signature
|
||
- Foundation files: `README.md`, `CLAUDE.md`, `CHANGELOG.md`
|
||
- Saved initial plan to `docs/plans/2026-07-01-00-08-go-latency-tool.md`
|