feat(go): step 1 — total wall-clock latency per URL

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 <noreply@anthropic.com>
This commit is contained in:
2026-07-01 00:21:12 +02:00
parent 8d5631795b
commit a588eb3b0e
5 changed files with 114 additions and 10 deletions

View File

@@ -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) ## 2026-07-01 00:08 — Project scaffold (Go, Step 0)
- Created project structure: `go/`, `python/`, `docs/plans/`, `docs/usage/` - Created project structure: `go/`, `python/`, `docs/plans/`, `docs/usage/`

View File

@@ -90,7 +90,7 @@ https://example.com (5 samples)
| Step | Feature | Status | | Step | Feature | Status |
|------|---------|--------| |------|---------|--------|
| 0 | Project scaffold — directory structure, `go.mod`, minimal binary | ✅ Done | | 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 | | 2 | Per-phase breakdown — DNS, TCP, TLS, TTFB, transfer (`net/http/httptrace`) | ⬜ Pending |
| 3 | Multiple URLs + `--count`/`-n` — min/avg/max aggregates | ⬜ Pending | | 3 | Multiple URLs + `--count`/`-n` — min/avg/max aggregates | ⬜ Pending |
| 4 | `--json` output flag | ⬜ Pending | | 4 | `--json` output flag | ⬜ Pending |

View File

@@ -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> [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 34.

View File

@@ -1,10 +1,13 @@
// Package probe measures per-phase HTTP request latency. // Package probe measures per-phase HTTP request latency.
package probe package probe
import "time" import (
"io"
"net/http"
"time"
)
// Phase holds the measured duration of a single request phase. // 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 { type Phase struct {
Duration time.Duration Duration time.Duration
// Present is false when the phase was skipped (e.g. no TLS for http://). // Present is false when the phase was skipped (e.g. no TLS for http://).
@@ -27,8 +30,27 @@ type Result struct {
Err error Err error
} }
// Measure performs an HTTP GET request to url and returns a populated Result. // Measure performs an HTTP GET to url and returns a Result.
// It is a placeholder until Step 1 wires in the actual implementation. // In Step 1 only the Total phase is populated.
func Measure(url string) Result { 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
} }

View File

@@ -4,6 +4,9 @@ import (
"flag" "flag"
"fmt" "fmt"
"os" "os"
"time"
"latprobe/internal/probe"
) )
const usageText = `latprobe — measure per-phase HTTP request latency const usageText = `latprobe — measure per-phase HTTP request latency
@@ -36,9 +39,22 @@ func main() {
os.Exit(1) os.Exit(1)
} }
// Placeholders — implementations wired in per step. // --json and -n are parsed but not yet functional (wired in Steps 34).
_, _ = count, jsonOut _ = jsonOut
_ = count
fmt.Fprintf(os.Stderr, "latprobe: not yet implemented (scaffold only)\n") 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) os.Exit(1)
} }
}