From a588eb3b0eb2d52516dafd8a8fdeac9c86d7a51c Mon Sep 17 00:00:00 2001 From: Jan Novak Date: Wed, 1 Jul 2026 00:21:12 +0200 Subject: [PATCH] =?UTF-8?q?feat(go):=20step=201=20=E2=80=94=20total=20wall?= =?UTF-8?q?-clock=20latency=20per=20URL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit probe.Measure performs an HTTP GET and records total elapsed time from request start to body fully read. main.go prints URL, status code, and total for each positional URL argument, exiting 1 on any failure. Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 9 +++++ README.md | 2 +- docs/usage/step-1-total-latency.md | 57 ++++++++++++++++++++++++++++++ go/internal/probe/probe.go | 32 ++++++++++++++--- go/main.go | 24 ++++++++++--- 5 files changed, 114 insertions(+), 10 deletions(-) create mode 100644 docs/usage/step-1-total-latency.md 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) + } }