123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301 |
- package main
- import (
- "fmt"
- "net"
- "net/http"
- _ "net/http/pprof"
- "os"
- "strconv"
- "strings"
- "time"
- "golang.org/x/time/rate"
- "gopkg.in/alecthomas/kingpin.v3-unstable"
- )
- var (
- concurrency = kingpin.Flag("concurrency", "Number of connections to run concurrently").Short('c').Default("1").Int()
- reqRate = rateFlag(kingpin.Flag("rate", "Number of requests per time unit, examples: --rate 50 --rate 10/ms").Default("infinity"))
- requests = kingpin.Flag("requests", "Number of requests to run").Short('n').Default("-1").Int64()
- duration = kingpin.Flag("duration", "Duration of test, examples: -d 10s -d 3m").Short('d').PlaceHolder("DURATION").Duration()
- interval = kingpin.Flag("interval", "Print snapshot result every interval, use 0 to print once at the end").Short('i').Default("200ms").Duration()
- seconds = kingpin.Flag("seconds", "Use seconds as time unit to print").Bool()
- jsonFormat = kingpin.Flag("json", "Print snapshot result as JSON").Bool()
- body = kingpin.Flag("body", "HTTP request body, if body starts with '@' the rest will be considered a file's path from which to read the actual body content").Short('b').String()
- stream = kingpin.Flag("stream", "Specify whether to stream file specified by '--body @file' using chunked encoding or to read into memory").Default("false").Bool()
- methodSet = false
- method = kingpin.Flag("method", "HTTP method").Action(func(_ *kingpin.ParseElement, _ *kingpin.ParseContext) error {
- methodSet = true
- return nil
- }).Default("GET").Short('m').String()
- headers = kingpin.Flag("header", "Custom HTTP headers").Short('H').PlaceHolder("K:V").Strings()
- host = kingpin.Flag("host", "Host header").String()
- contentType = kingpin.Flag("content", "Content-Type header").Short('T').String()
- cert = kingpin.Flag("cert", "Path to the client's TLS Certificate").ExistingFile()
- key = kingpin.Flag("key", "Path to the client's TLS Certificate Private Key").ExistingFile()
- insecure = kingpin.Flag("insecure", "Controls whether a client verifies the server's certificate chain and host name").Short('k').Bool()
- chartsListenAddr = kingpin.Flag("listen", "Listen addr to serve Web UI").Default(":18888").String()
- timeout = kingpin.Flag("timeout", "Timeout for each http request").PlaceHolder("DURATION").Duration()
- dialTimeout = kingpin.Flag("dial-timeout", "Timeout for dial addr").PlaceHolder("DURATION").Duration()
- reqWriteTimeout = kingpin.Flag("req-timeout", "Timeout for full request writing").PlaceHolder("DURATION").Duration()
- respReadTimeout = kingpin.Flag("resp-timeout", "Timeout for full response reading").PlaceHolder("DURATION").Duration()
- socks5 = kingpin.Flag("socks5", "Socks5 proxy").PlaceHolder("ip:port").String()
- autoOpenBrowser = kingpin.Flag("auto-open-browser", "Specify whether auto open browser to show web charts").Bool()
- clean = kingpin.Flag("clean", "Clean the histogram bar once its finished. Default is true").Default("true").NegatableBool()
- summary = kingpin.Flag("summary", "Only print the summary without realtime reports").Default("false").Bool()
- pprofAddr = kingpin.Flag("pprof", "Enable pprof at special address").Hidden().String()
- url = kingpin.Arg("url", "Request url").Required().String()
- )
- // dynamically set by GoReleaser
- var version = "dev"
- func errAndExit(msg string) {
- fmt.Fprintln(os.Stderr, "plow: "+msg)
- os.Exit(1)
- }
- var CompactUsageTemplate = `{{define "FormatCommand" -}}
- {{if .FlagSummary}} {{.FlagSummary}}{{end -}}
- {{range .Args}} {{if not .Required}}[{{end}}<{{.Name}}>{{if .Value|IsCumulative}} ...{{end}}{{if not .Required}}]{{end}}{{end -}}
- {{end -}}
- {{define "FormatCommandList" -}}
- {{range . -}}
- {{if not .Hidden -}}
- {{.Depth|Indent}}{{.Name}}{{if .Default}}*{{end}}{{template "FormatCommand" .}}
- {{end -}}
- {{template "FormatCommandList" .Commands -}}
- {{end -}}
- {{end -}}
- {{define "FormatUsage" -}}
- {{template "FormatCommand" .}}{{if .Commands}} <command> [<args> ...]{{end}}
- {{if .Help}}
- {{.Help|Wrap 0 -}}
- {{end -}}
- {{end -}}
- {{if .Context.SelectedCommand -}}
- {{T "usage:"}} {{.App.Name}} {{template "FormatUsage" .Context.SelectedCommand}}
- {{else -}}
- {{T "usage:"}} {{.App.Name}}{{template "FormatUsage" .App}}
- {{end -}}
- Examples:
- plow http://127.0.0.1:8080/ -c 20 -n 100000
- plow https://httpbin.org/post -c 20 -d 5m --body @file.json -T 'application/json' -m POST
- {{if .Context.Flags -}}
- {{T "Flags:"}}
- {{.Context.Flags|FlagsToTwoColumns|FormatTwoColumns}}
- Flags default values also read from env PLOW_SOME_FLAG, such as PLOW_TIMEOUT=5s equals to --timeout=5s
- {{end -}}
- {{if .Context.Args -}}
- {{T "Args:"}}
- {{.Context.Args|ArgsToTwoColumns|FormatTwoColumns}}
- {{end -}}
- {{if .Context.SelectedCommand -}}
- {{if .Context.SelectedCommand.Commands -}}
- {{T "Commands:"}}
- {{.Context.SelectedCommand}}
- {{.Context.SelectedCommand.Commands|CommandsToTwoColumns|FormatTwoColumns}}
- {{end -}}
- {{else if .App.Commands -}}
- {{T "Commands:"}}
- {{.App.Commands|CommandsToTwoColumns|FormatTwoColumns}}
- {{end -}}
- `
- type rateFlagValue struct {
- infinity bool
- limit rate.Limit
- v string
- }
- func (f *rateFlagValue) Set(v string) error {
- if v == "infinity" {
- f.infinity = true
- return nil
- }
- retErr := fmt.Errorf("--rate format %q doesn't match the \"freq/duration\" (i.e. 50/1s)", v)
- ps := strings.SplitN(v, "/", 2)
- switch len(ps) {
- case 1:
- ps = append(ps, "1s")
- case 0:
- return retErr
- }
- freq, err := strconv.Atoi(ps[0])
- if err != nil {
- return retErr
- }
- if freq == 0 {
- f.infinity = true
- return nil
- }
- switch ps[1] {
- case "ns", "us", "µs", "ms", "s", "m", "h":
- ps[1] = "1" + ps[1]
- }
- per, err := time.ParseDuration(ps[1])
- if err != nil {
- return retErr
- }
- f.limit = rate.Limit(float64(freq) / per.Seconds())
- f.v = v
- return nil
- }
- func (f *rateFlagValue) Limit() *rate.Limit {
- if f.infinity {
- return nil
- }
- return &f.limit
- }
- func (f *rateFlagValue) String() string {
- return f.v
- }
- func rateFlag(c *kingpin.Clause) (target *rateFlagValue) {
- target = new(rateFlagValue)
- c.SetValue(target)
- return
- }
- func main() {
- kingpin.UsageTemplate(CompactUsageTemplate).
- Version(version).
- Author("six-ddc@github").
- Resolver(kingpin.PrefixedEnvarResolver("PLOW_", ";")).
- Help = `A high-performance HTTP benchmarking tool with real-time web UI and terminal displaying`
- kingpin.Parse()
- if *requests >= 0 && *requests < int64(*concurrency) {
- errAndExit("requests must greater than or equal concurrency")
- return
- }
- if (*cert != "" && *key == "") || (*cert == "" && *key != "") {
- errAndExit("must specify cert and key at the same time")
- return
- }
- if *pprofAddr != "" {
- go http.ListenAndServe(*pprofAddr, nil)
- }
- var err error
- var bodyBytes []byte
- var bodyFile string
- if *body != "" {
- if strings.HasPrefix(*body, "@") {
- fileName := (*body)[1:]
- if _, err = os.Stat(fileName); err != nil {
- errAndExit(err.Error())
- return
- }
- if *stream {
- bodyFile = fileName
- } else {
- bodyBytes, err = os.ReadFile(fileName)
- if err != nil {
- errAndExit(err.Error())
- return
- }
- }
- } else {
- bodyBytes = []byte(*body)
- }
- if !methodSet {
- *method = "POST"
- }
- }
- clientOpt := ClientOpt{
- url: *url,
- method: *method,
- headers: *headers,
- bodyBytes: bodyBytes,
- bodyFile: bodyFile,
- certPath: *cert,
- keyPath: *key,
- insecure: *insecure,
- maxConns: *concurrency,
- doTimeout: *timeout,
- readTimeout: *respReadTimeout,
- writeTimeout: *reqWriteTimeout,
- dialTimeout: *dialTimeout,
- socks5Proxy: *socks5,
- contentType: *contentType,
- host: *host,
- }
- requester, err := NewRequester(*concurrency, *requests, *duration, reqRate.Limit(), &clientOpt)
- if err != nil {
- errAndExit(err.Error())
- return
- }
- // description
- var desc string
- desc = fmt.Sprintf("Benchmarking %s", *url)
- if *requests > 0 {
- desc += fmt.Sprintf(" with %d request(s)", *requests)
- }
- if *duration > 0 {
- desc += fmt.Sprintf(" for %s", duration.String())
- }
- desc += fmt.Sprintf(" using %d connection(s).", *concurrency)
- fmt.Fprintln(os.Stderr, desc)
- // charts listener
- var ln net.Listener
- if *chartsListenAddr != "" {
- ln, err = net.Listen("tcp", *chartsListenAddr)
- if err != nil {
- errAndExit(err.Error())
- return
- }
- fmt.Fprintf(os.Stderr, "@ Real-time charts is listening on http://%s\n", ln.Addr().String())
- }
- fmt.Fprintln(os.Stderr, "")
- // do request
- go requester.Run()
- // metrics collection
- report := NewStreamReport()
- go report.Collect(requester.RecordChan())
- if ln != nil {
- // serve charts data
- charts, err := NewCharts(ln, report.Charts, desc)
- if err != nil {
- errAndExit(err.Error())
- return
- }
- go charts.Serve(*autoOpenBrowser)
- }
- // terminal printer
- printer := NewPrinter(*requests, *duration, !*clean, *summary)
- printer.PrintLoop(report.Snapshot, *interval, *seconds, *jsonFormat, report.Done())
- }
|