Classify network failures (dns/connect/timeout/tls) with distinct exit codes 2-5. Add --timeout (default 10s) via context.WithTimeout. Add --fail for exit 6 on HTTP status >= 400. Preserve partial phase timing up to the failure point. -n sampling continues on network failures, aggregating successes and reporting fail counts. JSON extended with succeeded/failed/errors fields. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
149 lines
3.9 KiB
Markdown
149 lines
3.9 KiB
Markdown
# Step 5 — Failure Handling
|
||
|
||
## What this step delivers
|
||
|
||
`latprobe` now handles all three real-world failure modes explicitly:
|
||
|
||
| Failure | Exit code | Behaviour |
|
||
|---------|-----------|-----------|
|
||
| DNS resolution failure | 2 | Partial timing (DNS duration) shown; classified as `dns` |
|
||
| Connection failure (refused / unreachable) | 3 | Partial timing shown; classified as `connect` |
|
||
| Timeout (exceeded `--timeout`) | 4 | Partial timing shown; classified as `timeout` |
|
||
| TLS handshake failure | 5 | Partial timing shown; classified as `tls` |
|
||
| HTTP status ≥ 400 (with `--fail`) | 6 | Full timing shown, status annotated with ✗ |
|
||
| HTTP status ≥ 400 (without `--fail`) | 0 | Full timing shown, status visible but exit is 0 |
|
||
|
||
With `-n` sampling, network failures do **not** abort the run — remaining samples continue and successful ones are aggregated normally. The failure count and cause are reported alongside the aggregate.
|
||
|
||
When multiple URLs or failure types occur, the process exits with the **highest** exit code encountered.
|
||
|
||
## New flags
|
||
|
||
| Flag | Default | Description |
|
||
|------|---------|-------------|
|
||
| `--timeout` | `10s` | Per-request timeout (e.g. `500ms`, `2s`, `30s`) |
|
||
| `--fail` | off | Exit with code 6 when any URL returns HTTP status ≥ 400 |
|
||
|
||
## Examples
|
||
|
||
### DNS failure
|
||
|
||
```sh
|
||
$ ./latprobe https://nonexistent.invalid; echo $?
|
||
https://nonexistent.invalid (FAILED)
|
||
✗ dns: dial tcp: lookup nonexistent.invalid: no such host
|
||
2
|
||
```
|
||
|
||
### Connection refused
|
||
|
||
```sh
|
||
$ ./latprobe http://localhost:1; echo $?
|
||
http://localhost:1 (FAILED)
|
||
✗ connect: dial tcp [::1]:1: connect: connection refused
|
||
3
|
||
```
|
||
|
||
### Timeout
|
||
|
||
```sh
|
||
$ ./latprobe --timeout 1s https://example.com:81; echo $?
|
||
https://example.com:81 (FAILED)
|
||
✗ timeout: context deadline exceeded
|
||
4
|
||
```
|
||
|
||
### HTTP error — default (exit 0)
|
||
|
||
```sh
|
||
$ ./latprobe https://httpbin.org/status/500; echo $?
|
||
https://httpbin.org/status/500 (500)
|
||
DNS lookup : 27.75 ms
|
||
TCP connect : 114.47 ms
|
||
TLS handshake : 245.59 ms
|
||
Server (TTFB) : 113.06 ms
|
||
Transfer : 0.21 ms
|
||
─────────────────────────────
|
||
Total : 502.05 ms
|
||
0
|
||
```
|
||
|
||
### HTTP error — with `--fail` (exit 6)
|
||
|
||
```sh
|
||
$ ./latprobe --fail https://httpbin.org/status/404; echo $?
|
||
https://httpbin.org/status/404 (404 ✗)
|
||
DNS lookup : 3.35 ms
|
||
...
|
||
Total : 476.01 ms
|
||
6
|
||
```
|
||
|
||
### Mixed sampling (some network failures)
|
||
|
||
```sh
|
||
$ ./latprobe -n 5 https://flaky-host.example.com; echo $?
|
||
https://flaky-host.example.com (200, 3 samples, 2 failed)
|
||
min avg max
|
||
DNS lookup : 5.00 ms 5.10 ms 5.20 ms
|
||
...
|
||
─────────────────────────────────────────────────
|
||
Total : 80.00 ms 85.00 ms 92.00 ms
|
||
✗ 2 × connect: connection refused
|
||
3
|
||
```
|
||
|
||
### JSON output with failure
|
||
|
||
```sh
|
||
$ ./latprobe --json https://nonexistent.invalid | jq .
|
||
[
|
||
{
|
||
"url": "https://nonexistent.invalid",
|
||
"status": 0,
|
||
"succeeded": 0,
|
||
"failed": 1,
|
||
"errors": [
|
||
{
|
||
"phase": "dns",
|
||
"count": 1,
|
||
"message": "dial tcp: lookup nonexistent.invalid: no such host"
|
||
}
|
||
]
|
||
}
|
||
]
|
||
```
|
||
|
||
## JSON schema changes
|
||
|
||
`jsonEntry` now includes:
|
||
|
||
```json
|
||
{
|
||
"url": "https://example.com",
|
||
"status": 200,
|
||
"succeeded": 3,
|
||
"failed": 2,
|
||
"phases": { ... },
|
||
"errors": [
|
||
{ "phase": "connect", "count": 2, "message": "connection refused" }
|
||
]
|
||
}
|
||
```
|
||
|
||
- `phases` is omitted when `succeeded == 0`
|
||
- `errors` is omitted when `failed == 0`
|
||
- `status` is 0 when no sample reached a response
|
||
|
||
## Exit code reference
|
||
|
||
| Code | Meaning |
|
||
|------|---------|
|
||
| 0 | All probes succeeded |
|
||
| 1 | Usage error (no URLs, bad flags) |
|
||
| 2 | DNS resolution failure |
|
||
| 3 | Connection failure |
|
||
| 4 | Timeout |
|
||
| 5 | TLS handshake failure |
|
||
| 6 | HTTP status ≥ 400 (only with `--fail`) |
|