Add probe.Summarize to aggregate N Results into per-phase min/avg/max stats. Wire up -n/--count flag in main to repeat each URL N times and display an aligned three-column table. Single-sample output (n=1) is unchanged. URLs are grouped with a blank line between them. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
132 lines
3.2 KiB
Go
132 lines
3.2 KiB
Go
package main
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"latprobe/internal/probe"
|
|
)
|
|
|
|
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)
|
|
}
|
|
|
|
// --json parsed but not yet functional (wired in Step 4).
|
|
_ = jsonOut
|
|
|
|
failed := false
|
|
for i, url := range urls {
|
|
if i > 0 {
|
|
fmt.Println()
|
|
}
|
|
|
|
results := make([]probe.Result, 0, *count)
|
|
for j := 0; j < *count; j++ {
|
|
r := probe.Measure(url)
|
|
if r.Err != nil {
|
|
fmt.Fprintf(os.Stderr, "error %s (sample %d/%d): %v\n", url, j+1, *count, r.Err)
|
|
failed = true
|
|
break
|
|
}
|
|
results = append(results, r)
|
|
}
|
|
if len(results) == 0 {
|
|
continue
|
|
}
|
|
|
|
if *count == 1 {
|
|
printResult(results[0])
|
|
} else {
|
|
printAggregate(probe.Summarize(results))
|
|
}
|
|
}
|
|
|
|
if failed {
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
// ── single-sample output ──────────────────────────────────────────────────────
|
|
|
|
func printResult(r probe.Result) {
|
|
fmt.Printf("%s (%d)\n", r.URL, r.StatusCode)
|
|
|
|
phases := []struct {
|
|
label string
|
|
p probe.Phase
|
|
}{
|
|
{"DNS lookup ", r.DNS},
|
|
{"TCP connect ", r.Connect},
|
|
{"TLS handshake ", r.TLS},
|
|
{"Server (TTFB) ", r.TTFB},
|
|
{"Transfer ", r.Transfer},
|
|
}
|
|
for _, ph := range phases {
|
|
if ph.p.Present {
|
|
fmt.Printf(" %s : %8.2f ms\n", ph.label, ms(ph.p.Duration))
|
|
}
|
|
}
|
|
fmt.Println(" " + strings.Repeat("─", 29))
|
|
fmt.Printf(" %s : %8.2f ms\n", "Total ", ms(r.Total.Duration))
|
|
}
|
|
|
|
// ── multi-sample output ───────────────────────────────────────────────────────
|
|
|
|
func printAggregate(a probe.Aggregate) {
|
|
fmt.Printf("%s (%d, %d samples)\n", a.URL, a.StatusCode, a.Count)
|
|
fmt.Printf(" %-14s %9s %9s %9s\n", "", "min", "avg", "max")
|
|
|
|
phases := []struct {
|
|
label string
|
|
p probe.PhaseStats
|
|
}{
|
|
{"DNS lookup ", a.DNS},
|
|
{"TCP connect ", a.Connect},
|
|
{"TLS handshake ", a.TLS},
|
|
{"Server (TTFB) ", a.TTFB},
|
|
{"Transfer ", a.Transfer},
|
|
}
|
|
for _, ph := range phases {
|
|
if ph.p.Present {
|
|
fmt.Printf(" %s : %6.2f ms %6.2f ms %6.2f ms\n",
|
|
ph.label, ms(ph.p.Min), ms(ph.p.Avg), ms(ph.p.Max))
|
|
}
|
|
}
|
|
fmt.Println(" " + strings.Repeat("─", 49))
|
|
fmt.Printf(" %s : %6.2f ms %6.2f ms %6.2f ms\n",
|
|
"Total ", ms(a.Total.Min), ms(a.Total.Avg), ms(a.Total.Max))
|
|
}
|
|
|
|
func ms(d time.Duration) float64 {
|
|
return float64(d.Microseconds()) / 1000
|
|
}
|