diff --git a/CHANGELOG.md b/CHANGELOG.md index d7150ef..f3fa1ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ All completed features are logged here in reverse-chronological order. --- +## 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/` diff --git a/README.md b/README.md index 75f949c..f483c09 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ https://example.com (5 samples) | Step | Feature | Status | |------|---------|--------| | 0 | Project scaffold — directory structure, `go.mod`, minimal binary | ✅ Done | -| 1 | Simple total latency — single URL, wall-clock time | ⬜ Pending | +| 1 | Simple total latency — single URL, wall-clock time | ✅ Done | | 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 | diff --git a/docs/usage/step-1-total-latency.md b/docs/usage/step-1-total-latency.md new file mode 100644 index 0000000..464da09 --- /dev/null +++ b/docs/usage/step-1-total-latency.md @@ -0,0 +1,57 @@ +# Step 1 — Simple Total Latency + +## What this step delivers + +`latprobe` accepts one or more URLs and prints the total wall-clock time of +each HTTP GET request (including body download). No per-phase breakdown yet — +that comes in Step 2. + +## Build + +```sh +cd go +go build -o latprobe . +``` + +## Usage + +```sh +latprobe [url ...] +``` + +## Examples + +Single URL: + +```sh +$ ./latprobe https://example.com +https://example.com 200 70ms +``` + +Multiple URLs: + +```sh +$ ./latprobe https://example.com https://www.google.com +https://example.com 200 70ms +https://www.google.com 200 127ms +``` + +Error (unreachable host): + +```sh +$ ./latprobe https://doesnotexist.invalid +error https://doesnotexist.invalid: Get "https://doesnotexist.invalid": ... +# exits with code 1 +``` + +## Output columns + +| Column | Description | +|--------|-------------| +| URL | The requested URL | +| Status | HTTP response status code | +| Total | Wall-clock time from request start to body fully received | + +## Flags accepted (not yet functional) + +`-n`/`--count` and `--json` are parsed but produce no effect until Steps 3–4. diff --git a/go/internal/probe/probe.go b/go/internal/probe/probe.go index 28169e1..395a28d 100644 --- a/go/internal/probe/probe.go +++ b/go/internal/probe/probe.go @@ -1,10 +1,13 @@ // Package probe measures per-phase HTTP request latency. package probe -import "time" +import ( + "io" + "net/http" + "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://). @@ -27,8 +30,27 @@ type Result struct { 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. +// Measure performs an HTTP GET to url and returns a Result. +// In Step 1 only the Total phase is populated. func Measure(url string) Result { - return Result{URL: url} + r := Result{URL: url} + + start := time.Now() + + resp, err := http.Get(url) //nolint:noctx + if err != nil { + r.Err = err + r.Total = Phase{Duration: time.Since(start), Present: true} + return r + } + defer resp.Body.Close() + + _, err = io.Copy(io.Discard, resp.Body) + + r.Total = Phase{Duration: time.Since(start), Present: true} + r.StatusCode = resp.StatusCode + if err != nil { + r.Err = err + } + return r } diff --git a/go/main.go b/go/main.go index 285d22b..212ba6d 100644 --- a/go/main.go +++ b/go/main.go @@ -4,6 +4,9 @@ import ( "flag" "fmt" "os" + "time" + + "latprobe/internal/probe" ) const usageText = `latprobe — measure per-phase HTTP request latency @@ -36,9 +39,22 @@ func main() { os.Exit(1) } - // Placeholders — implementations wired in per step. - _, _ = count, jsonOut + // --json and -n are parsed but not yet functional (wired in Steps 3–4). + _ = jsonOut + _ = count - fmt.Fprintf(os.Stderr, "latprobe: not yet implemented (scaffold only)\n") - os.Exit(1) + failed := false + for _, url := range urls { + r := probe.Measure(url) + if r.Err != nil { + fmt.Fprintf(os.Stderr, "error %s: %v\n", url, r.Err) + failed = true + continue + } + fmt.Printf("%-45s %d %v\n", r.URL, r.StatusCode, r.Total.Duration.Round(time.Millisecond)) + } + + if failed { + os.Exit(1) + } }