Extract run(args, stdout, stderr) int from main() for in-process testability. Fix TLS failure classification (tlsErr now captured from TLSHandshakeDone hook). Add run_test.go with 14 table-driven in-process tests and cli_test.go with TestMain + 3 subprocess smoke tests. All servers use httptest; .invalid TLD for deterministic DNS failures. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
120 lines
3.2 KiB
Markdown
120 lines
3.2 KiB
Markdown
# latprobe — Website Latency Probe
|
|
|
|
A command-line tool that measures the latency of one or more websites, with a
|
|
**full per-request phase breakdown** — DNS resolution, TCP connect, TLS
|
|
handshake, server processing (time-to-first-byte), and content transfer.
|
|
Not just a ping: `latprobe` shows you _where_ the time goes.
|
|
|
|
## Why
|
|
|
|
A single total request time hides the root cause of slowness. Is it DNS? A slow
|
|
TLS negotiation? A laggy server? `latprobe` breaks the request into its
|
|
constituent phases so the bottleneck is immediately obvious.
|
|
|
|
## Implementations
|
|
|
|
| Language | Status | Location |
|
|
|----------|-----------|------------|
|
|
| Go | In progress | `go/` |
|
|
| Python | Planned | `python/` |
|
|
|
|
Both implementations produce identical CLI behaviour and output formats.
|
|
|
|
---
|
|
|
|
## Usage (Go)
|
|
|
|
```
|
|
latprobe [flags] <url> [url ...]
|
|
|
|
Flags:
|
|
-n, --count int Number of requests per URL (default 1)
|
|
--json Output results as JSON instead of text
|
|
|
|
Examples:
|
|
latprobe https://example.com
|
|
latprobe -n 5 https://example.com https://www.google.com
|
|
latprobe --json https://example.com | jq .
|
|
```
|
|
|
|
---
|
|
|
|
## Output
|
|
|
|
### Text (default)
|
|
|
|
```
|
|
https://example.com
|
|
DNS lookup : 12.34 ms
|
|
TCP connect : 8.91 ms
|
|
TLS handshake : 45.20 ms
|
|
Server (TTFB) : 78.56 ms
|
|
Transfer : 2.10 ms
|
|
─────────────────────────
|
|
Total : 147.11 ms
|
|
```
|
|
|
|
With `-n 5` (min / avg / max columns):
|
|
|
|
```
|
|
https://example.com (5 samples)
|
|
min avg max
|
|
DNS lookup : 10.1ms 12.3ms 15.7ms
|
|
TCP connect : 7.8ms 9.0ms 11.2ms
|
|
...
|
|
```
|
|
|
|
### JSON (`--json`)
|
|
|
|
```json
|
|
{
|
|
"url": "https://example.com",
|
|
"samples": 1,
|
|
"phases": {
|
|
"dns": { "ms": 12.34 },
|
|
"connect": { "ms": 8.91 },
|
|
"tls": { "ms": 45.20 },
|
|
"ttfb": { "ms": 78.56 },
|
|
"transfer": { "ms": 2.10 },
|
|
"total": { "ms": 147.11 }
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Implementation Roadmap
|
|
|
|
### Go
|
|
|
|
| Step | Feature | Status |
|
|
|------|---------|--------|
|
|
| 0 | Project scaffold — directory structure, `go.mod`, minimal binary | ✅ Done |
|
|
| 1 | Simple total latency — single URL, wall-clock time | ✅ Done |
|
|
| 2 | Per-phase breakdown — DNS, TCP, TLS, TTFB, transfer (`net/http/httptrace`) | ✅ Done |
|
|
| 3 | Multiple URLs + `--count`/`-n` — min/avg/max aggregates | ✅ Done |
|
|
| 4 | `--json` output flag | ✅ Done |
|
|
| 5 | Failure handling — `--timeout`, `--fail`, distinct exit codes, partial timing | ✅ Done |
|
|
| 6 | Integration tests — in-process matrix + subprocess smoke tests | ✅ Done |
|
|
|
|
### Python
|
|
|
|
Begins after the Go implementation is approved. Will mirror the same CLI and
|
|
output format. Timed using low-level socket hooks (DNS via `socket.getaddrinfo`,
|
|
connection timings via custom socket wrap or `httpx`/`urllib3` hooks).
|
|
|
|
---
|
|
|
|
## Development Environment
|
|
|
|
- Go 1.26.4 / darwin arm64
|
|
- Python 3.14.6
|
|
- No external dependencies (Go uses stdlib only; Python TBD at port time)
|
|
|
|
## Project Conventions
|
|
|
|
See [CLAUDE.md](CLAUDE.md) for the development conventions followed in this project:
|
|
- Plans saved to `docs/plans/` with timestamped filenames
|
|
- Completed features logged in `CHANGELOG.md` with timestamps
|
|
- Per-feature user docs in `docs/usage/`
|