chore: initial scaffold for latprobe CLI tool
Sets up project structure, working conventions (CLAUDE.md), README with the full assignment and roadmap, seeded CHANGELOG, and a buildable Go scaffold (Step 0) with flag parsing and stub probe types. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
14
CHANGELOG.md
Normal file
14
CHANGELOG.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All completed features are logged here in reverse-chronological order.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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`
|
||||||
41
CLAUDE.md
Normal file
41
CLAUDE.md
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# CLAUDE.md — Working Conventions
|
||||||
|
|
||||||
|
This file records the conventions Claude must follow throughout this project.
|
||||||
|
|
||||||
|
## Plans
|
||||||
|
|
||||||
|
Every implementation plan is saved under `docs/plans/` with a filename that
|
||||||
|
starts with the **current timestamp in `yyyy-mm-dd-hh-mm` format** followed by
|
||||||
|
a short kebab-case description.
|
||||||
|
|
||||||
|
Example: `docs/plans/2026-07-01-00-08-go-latency-tool.md`
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
Every completed feature is appended to `CHANGELOG.md` at the project root with
|
||||||
|
a **timestamp** and a one-line description of what was added or changed.
|
||||||
|
|
||||||
|
## User Documentation
|
||||||
|
|
||||||
|
Every shipped feature must have a corresponding documentation file under
|
||||||
|
`docs/usage/`. Each file must include:
|
||||||
|
- What the feature does
|
||||||
|
- All relevant flags / arguments
|
||||||
|
- At least one concrete command-line example with expected output
|
||||||
|
|
||||||
|
## Implementation Order
|
||||||
|
|
||||||
|
1. Go implementation — developed first, incrementally, with user sign-off
|
||||||
|
between steps.
|
||||||
|
2. Python port — begins only after the user approves the Go implementation.
|
||||||
|
Its plan is drafted separately at that time.
|
||||||
|
|
||||||
|
## Steps (Go)
|
||||||
|
|
||||||
|
| Step | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| 0 | Scaffold — directory structure, `go.mod`, minimal `main.go` that prints usage |
|
||||||
|
| 1 | Simple total latency — single URL, print wall-clock time of the full request |
|
||||||
|
| 2 | Per-phase breakdown — DNS, TCP connect, TLS, TTFB, transfer, total via `net/http/httptrace` |
|
||||||
|
| 3 | Multiple URLs + sampling — `--count`/`-n` flag, min/avg/max per phase |
|
||||||
|
| 4 | JSON output — `--json` flag; text stays default |
|
||||||
117
README.md
Normal file
117
README.md
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
# 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 | ⬜ Pending |
|
||||||
|
| 2 | Per-phase breakdown — DNS, TCP, TLS, TTFB, transfer (`net/http/httptrace`) | ⬜ Pending |
|
||||||
|
| 3 | Multiple URLs + `--count`/`-n` — min/avg/max aggregates | ⬜ Pending |
|
||||||
|
| 4 | `--json` output flag | ⬜ Pending |
|
||||||
|
|
||||||
|
### 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/`
|
||||||
122
docs/plans/2026-07-01-00-08-go-latency-tool.md
Normal file
122
docs/plans/2026-07-01-00-08-go-latency-tool.md
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
# Plan: Website latency CLI tool (Go first, then Python)
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
We're building a small CLI tool that measures the latency of one or more
|
||||||
|
websites given their URLs. The point of difference from a naive timer is a
|
||||||
|
**full per-phase breakdown of an HTTP request** — DNS resolution, TCP connect,
|
||||||
|
TLS handshake, server processing (time-to-first-byte), and content transfer —
|
||||||
|
not just a single total number.
|
||||||
|
|
||||||
|
We start simple (total latency only) and grow the tool in clear steps. The Go
|
||||||
|
implementation comes first; once the user is satisfied, we port the same
|
||||||
|
behaviour to Python. This plan also establishes project-wide working
|
||||||
|
conventions (plans, changelog, per-feature docs) up front.
|
||||||
|
|
||||||
|
Design decisions confirmed with the user:
|
||||||
|
- **Output:** human-readable text by default, with a `--json` flag for machine output.
|
||||||
|
- **Sampling:** a `--count`/`-n` flag repeats N requests per URL and reports min/avg/max per phase.
|
||||||
|
- **Input:** accept multiple URLs as positional arguments, measured in turn.
|
||||||
|
|
||||||
|
Toolchain present: Go 1.26.4, Python 3.14.6.
|
||||||
|
|
||||||
|
## Proposed layout
|
||||||
|
|
||||||
|
```
|
||||||
|
tt-excercise/
|
||||||
|
├── CLAUDE.md # working conventions (created first)
|
||||||
|
├── README.md # the assignment + implementation steps
|
||||||
|
├── CHANGELOG.md # timestamped completed features
|
||||||
|
├── docs/
|
||||||
|
│ ├── plans/ # timestamped plans (this plan copied here)
|
||||||
|
│ └── usage/ # per-feature user documentation
|
||||||
|
├── go/
|
||||||
|
│ ├── go.mod # module: latprobe (name adjustable)
|
||||||
|
│ ├── main.go # CLI: flag parsing, orchestration, output
|
||||||
|
│ └── internal/probe/
|
||||||
|
│ └── probe.go # timing logic via net/http/httptrace
|
||||||
|
└── python/ # added later, during the port
|
||||||
|
```
|
||||||
|
|
||||||
|
Binary/module name proposed: **`latprobe`** (easy to change).
|
||||||
|
|
||||||
|
## Foundation files (created at the start)
|
||||||
|
|
||||||
|
### CLAUDE.md — working conventions
|
||||||
|
- Every plan is saved under `docs/plans/` with a filename starting with the
|
||||||
|
current timestamp in `yyyy-mm-dd-hh-mm` format
|
||||||
|
(e.g. `2026-07-01-00-08-go-latency-tool.md`).
|
||||||
|
- Every completed feature is appended to `CHANGELOG.md` with a timestamp.
|
||||||
|
- Every feature ships with user documentation under `docs/usage/` including a
|
||||||
|
concrete example of how to use it.
|
||||||
|
|
||||||
|
### README.md — the assignment
|
||||||
|
- Project purpose: CLI tool to measure website latency, with a full per-request
|
||||||
|
phase breakdown including DNS resolution.
|
||||||
|
- Two implementations: Go first, then a Python port.
|
||||||
|
- Usage sketch and the staged implementation roadmap (below).
|
||||||
|
- Note that Go 1.26 / Python 3.14 are the development versions.
|
||||||
|
|
||||||
|
### CHANGELOG.md
|
||||||
|
- Seeded with a header; entries appended per completed feature.
|
||||||
|
|
||||||
|
## Implementation steps (Go)
|
||||||
|
|
||||||
|
Each step is a self-contained, reviewable increment. We pause for the user's
|
||||||
|
sign-off between steps. On completion of each: append to `CHANGELOG.md` and add
|
||||||
|
its `docs/usage/` page.
|
||||||
|
|
||||||
|
**Step 0 — Scaffold.** Create `go/go.mod` (`go mod init latprobe`), a minimal
|
||||||
|
`main.go` that prints usage, and the directory structure. Confirms the toolchain
|
||||||
|
builds.
|
||||||
|
|
||||||
|
**Step 1 — Simple total latency.** Accept a single URL argument, perform an
|
||||||
|
HTTP GET, and print the total wall-clock time of the request. No breakdown yet.
|
||||||
|
Reuse the standard library only (`net/http`, `time`).
|
||||||
|
|
||||||
|
**Step 2 — Full per-phase breakdown (the core feature).** Instrument the request
|
||||||
|
with `net/http/httptrace.ClientTrace` to capture timestamps and derive each phase:
|
||||||
|
- DNS lookup (`DNSStart` → `DNSDone`)
|
||||||
|
- TCP connect (`ConnectStart` → `ConnectDone`)
|
||||||
|
- TLS handshake (`TLSHandshakeStart` → `TLSHandshakeDone`, HTTPS only)
|
||||||
|
- Server processing / TTFB (`WroteRequest` → `GotFirstResponseByte`)
|
||||||
|
- Content transfer (`GotFirstResponseByte` → response body fully read)
|
||||||
|
- Total
|
||||||
|
Print an aligned text breakdown. `httptrace` is the key reusable stdlib piece —
|
||||||
|
no third-party dependency needed.
|
||||||
|
|
||||||
|
**Step 3 — Multiple URLs + sampling.** Accept multiple positional URLs; add
|
||||||
|
`--count`/`-n` (default 1) to repeat each URL N times and report **min/avg/max**
|
||||||
|
per phase. Group output clearly per URL.
|
||||||
|
|
||||||
|
**Step 4 — JSON output.** Add a `--json` flag producing structured output
|
||||||
|
(per URL, per phase, with the min/avg/max aggregates and sample count). Keep the
|
||||||
|
human-readable text as the default.
|
||||||
|
|
||||||
|
Reasonable extras to consider later (not committed now): `--timeout`,
|
||||||
|
`--method`, redirect handling, HTTP/2 vs HTTP/1.1 reporting, exit codes on failure.
|
||||||
|
|
||||||
|
## Port to Python (after Go is approved)
|
||||||
|
|
||||||
|
Mirror the same CLI and output. Python has no direct `httptrace` equivalent, so
|
||||||
|
the plan will measure phases at a lower level — e.g. custom timing around
|
||||||
|
`socket.getaddrinfo` (DNS), connection establishment, and TLS wrap, or by using
|
||||||
|
`urllib3`/`httpx` connection hooks. The detailed Python design will be drafted
|
||||||
|
as its own plan (saved to `docs/plans/`) when we reach that stage.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- `cd go && go build ./...` succeeds at every step.
|
||||||
|
- Manual runs against known hosts, e.g.:
|
||||||
|
- `./latprobe https://example.com` → total only (Step 1), full breakdown (Step 2+).
|
||||||
|
- `./latprobe -n 5 https://example.com https://www.google.com` → min/avg/max per URL.
|
||||||
|
- `./latprobe --json https://example.com` → valid JSON (pipe through a JSON validator).
|
||||||
|
- Sanity-check phase numbers: phases should roughly sum to the total; HTTPS shows
|
||||||
|
a TLS phase, plain HTTP does not.
|
||||||
|
- `go vet ./...` clean.
|
||||||
|
|
||||||
|
## Bookkeeping at execution time
|
||||||
|
|
||||||
|
On exiting plan mode, before/with Step 0:
|
||||||
|
1. Copy this plan to `docs/plans/2026-07-01-00-08-go-latency-tool.md`.
|
||||||
|
2. Create `CLAUDE.md`, `README.md`, `CHANGELOG.md` as described above.
|
||||||
51
docs/usage/step-0-scaffold.md
Normal file
51
docs/usage/step-0-scaffold.md
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# Step 0 — Scaffold
|
||||||
|
|
||||||
|
## What this step delivers
|
||||||
|
|
||||||
|
A buildable Go binary (`latprobe`) that parses flags and URLs, prints usage
|
||||||
|
when invoked without arguments, and exits cleanly. No measurement logic yet —
|
||||||
|
the full implementation is wired in during Steps 1–4.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd go
|
||||||
|
go build -o latprobe .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# No arguments → prints usage and exits with code 1
|
||||||
|
./latprobe
|
||||||
|
|
||||||
|
# With a URL → scaffold message until Step 1 is implemented
|
||||||
|
./latprobe https://example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected output (scaffold stage):
|
||||||
|
|
||||||
|
```
|
||||||
|
latprobe — measure per-phase HTTP request latency
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
latprobe [flags] <url> [url ...]
|
||||||
|
|
||||||
|
Flags:
|
||||||
|
-n, --count int Number of requests per URL (default 1)
|
||||||
|
--json Output results as JSON instead of text
|
||||||
|
-h, --help Show this help
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
latprobe https://example.com
|
||||||
|
latprobe -n 5 https://example.com https://www.google.com
|
||||||
|
latprobe --json https://example.com | jq .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Available flags (accepted by the parser, not yet functional)
|
||||||
|
|
||||||
|
| Flag | Short | Default | Description |
|
||||||
|
|------|-------|---------|-------------|
|
||||||
|
| `--count` | `-n` | `1` | Number of requests per URL |
|
||||||
|
| `--json` | — | false | Output as JSON instead of text |
|
||||||
|
| `--help` | `-h` | — | Show usage |
|
||||||
34
go/internal/probe/probe.go
Normal file
34
go/internal/probe/probe.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
// Package probe measures per-phase HTTP request latency.
|
||||||
|
package probe
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// Phase holds the measured duration of a single request phase.
|
||||||
|
// A zero Duration means the phase did not occur (e.g. TLS on plain HTTP).
|
||||||
|
type Phase struct {
|
||||||
|
Duration time.Duration
|
||||||
|
// Present is false when the phase was skipped (e.g. no TLS for http://).
|
||||||
|
Present bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Result holds all timing phases for a single HTTP request.
|
||||||
|
type Result struct {
|
||||||
|
URL string
|
||||||
|
DNS Phase
|
||||||
|
Connect Phase
|
||||||
|
TLS Phase
|
||||||
|
TTFB Phase // server processing: WroteRequest → GotFirstResponseByte
|
||||||
|
Transfer Phase // body read: GotFirstResponseByte → body closed
|
||||||
|
Total Phase
|
||||||
|
|
||||||
|
// StatusCode is the HTTP response status code (0 on error).
|
||||||
|
StatusCode int
|
||||||
|
// Err is non-nil if the request failed.
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Measure performs an HTTP GET request to url and returns a populated Result.
|
||||||
|
// It is a placeholder until Step 1 wires in the actual implementation.
|
||||||
|
func Measure(url string) Result {
|
||||||
|
return Result{URL: url}
|
||||||
|
}
|
||||||
BIN
go/latprobe
Executable file
BIN
go/latprobe
Executable file
Binary file not shown.
44
go/main.go
Normal file
44
go/main.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
const usageText = `latprobe — measure per-phase HTTP request latency
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
latprobe [flags] <url> [url ...]
|
||||||
|
|
||||||
|
Flags:
|
||||||
|
-n, --count int Number of requests per URL (default 1)
|
||||||
|
--json Output results as JSON instead of text
|
||||||
|
-h, --help Show this help
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
latprobe https://example.com
|
||||||
|
latprobe -n 5 https://example.com https://www.google.com
|
||||||
|
latprobe --json https://example.com | jq .
|
||||||
|
`
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
count := flag.Int("count", 1, "number of requests per URL")
|
||||||
|
flag.IntVar(count, "n", 1, "number of requests per URL (shorthand)")
|
||||||
|
jsonOut := flag.Bool("json", false, "output results as JSON")
|
||||||
|
|
||||||
|
flag.Usage = func() { fmt.Fprint(os.Stderr, usageText) }
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
urls := flag.Args()
|
||||||
|
if len(urls) == 0 {
|
||||||
|
fmt.Fprint(os.Stderr, usageText)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Placeholders — implementations wired in per step.
|
||||||
|
_, _ = count, jsonOut
|
||||||
|
|
||||||
|
fmt.Fprintf(os.Stderr, "latprobe: not yet implemented (scaffold only)\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user