main.go 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  1. package main
  2. import (
  3. "fmt"
  4. "net"
  5. "net/http"
  6. _ "net/http/pprof"
  7. "os"
  8. "strconv"
  9. "strings"
  10. "time"
  11. "golang.org/x/time/rate"
  12. "gopkg.in/alecthomas/kingpin.v3-unstable"
  13. )
  14. var (
  15. concurrency = kingpin.Flag("concurrency", "Number of connections to run concurrently").Short('c').Default("1").Int()
  16. reqRate = rateFlag(kingpin.Flag("rate", "Number of requests per time unit, examples: --rate 50 --rate 10/ms").Default("infinity"))
  17. requests = kingpin.Flag("requests", "Number of requests to run").Short('n').Default("-1").Int64()
  18. duration = kingpin.Flag("duration", "Duration of test, examples: -d 10s -d 3m").Short('d').PlaceHolder("DURATION").Duration()
  19. interval = kingpin.Flag("interval", "Print snapshot result every interval, use 0 to print once at the end").Short('i').Default("200ms").Duration()
  20. seconds = kingpin.Flag("seconds", "Use seconds as time unit to print").Bool()
  21. jsonFormat = kingpin.Flag("json", "Print snapshot result as JSON").Bool()
  22. 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()
  23. stream = kingpin.Flag("stream", "Specify whether to stream file specified by '--body @file' using chunked encoding or to read into memory").Default("false").Bool()
  24. methodSet = false
  25. method = kingpin.Flag("method", "HTTP method").Action(func(_ *kingpin.ParseElement, _ *kingpin.ParseContext) error {
  26. methodSet = true
  27. return nil
  28. }).Default("GET").Short('m').String()
  29. headers = kingpin.Flag("header", "Custom HTTP headers").Short('H').PlaceHolder("K:V").Strings()
  30. host = kingpin.Flag("host", "Host header").String()
  31. contentType = kingpin.Flag("content", "Content-Type header").Short('T').String()
  32. cert = kingpin.Flag("cert", "Path to the client's TLS Certificate").ExistingFile()
  33. key = kingpin.Flag("key", "Path to the client's TLS Certificate Private Key").ExistingFile()
  34. insecure = kingpin.Flag("insecure", "Controls whether a client verifies the server's certificate chain and host name").Short('k').Bool()
  35. chartsListenAddr = kingpin.Flag("listen", "Listen addr to serve Web UI").Default(":18888").String()
  36. timeout = kingpin.Flag("timeout", "Timeout for each http request").PlaceHolder("DURATION").Duration()
  37. dialTimeout = kingpin.Flag("dial-timeout", "Timeout for dial addr").PlaceHolder("DURATION").Duration()
  38. reqWriteTimeout = kingpin.Flag("req-timeout", "Timeout for full request writing").PlaceHolder("DURATION").Duration()
  39. respReadTimeout = kingpin.Flag("resp-timeout", "Timeout for full response reading").PlaceHolder("DURATION").Duration()
  40. socks5 = kingpin.Flag("socks5", "Socks5 proxy").PlaceHolder("ip:port").String()
  41. autoOpenBrowser = kingpin.Flag("auto-open-browser", "Specify whether auto open browser to show web charts").Bool()
  42. clean = kingpin.Flag("clean", "Clean the histogram bar once its finished. Default is true").Default("true").NegatableBool()
  43. summary = kingpin.Flag("summary", "Only print the summary without realtime reports").Default("false").Bool()
  44. pprofAddr = kingpin.Flag("pprof", "Enable pprof at special address").Hidden().String()
  45. url = kingpin.Arg("url", "Request url").Required().String()
  46. )
  47. // dynamically set by GoReleaser
  48. var version = "dev"
  49. func errAndExit(msg string) {
  50. fmt.Fprintln(os.Stderr, "plow: "+msg)
  51. os.Exit(1)
  52. }
  53. var CompactUsageTemplate = `{{define "FormatCommand" -}}
  54. {{if .FlagSummary}} {{.FlagSummary}}{{end -}}
  55. {{range .Args}} {{if not .Required}}[{{end}}<{{.Name}}>{{if .Value|IsCumulative}} ...{{end}}{{if not .Required}}]{{end}}{{end -}}
  56. {{end -}}
  57. {{define "FormatCommandList" -}}
  58. {{range . -}}
  59. {{if not .Hidden -}}
  60. {{.Depth|Indent}}{{.Name}}{{if .Default}}*{{end}}{{template "FormatCommand" .}}
  61. {{end -}}
  62. {{template "FormatCommandList" .Commands -}}
  63. {{end -}}
  64. {{end -}}
  65. {{define "FormatUsage" -}}
  66. {{template "FormatCommand" .}}{{if .Commands}} <command> [<args> ...]{{end}}
  67. {{if .Help}}
  68. {{.Help|Wrap 0 -}}
  69. {{end -}}
  70. {{end -}}
  71. {{if .Context.SelectedCommand -}}
  72. {{T "usage:"}} {{.App.Name}} {{template "FormatUsage" .Context.SelectedCommand}}
  73. {{else -}}
  74. {{T "usage:"}} {{.App.Name}}{{template "FormatUsage" .App}}
  75. {{end -}}
  76. Examples:
  77. plow http://127.0.0.1:8080/ -c 20 -n 100000
  78. plow https://httpbin.org/post -c 20 -d 5m --body @file.json -T 'application/json' -m POST
  79. {{if .Context.Flags -}}
  80. {{T "Flags:"}}
  81. {{.Context.Flags|FlagsToTwoColumns|FormatTwoColumns}}
  82. Flags default values also read from env PLOW_SOME_FLAG, such as PLOW_TIMEOUT=5s equals to --timeout=5s
  83. {{end -}}
  84. {{if .Context.Args -}}
  85. {{T "Args:"}}
  86. {{.Context.Args|ArgsToTwoColumns|FormatTwoColumns}}
  87. {{end -}}
  88. {{if .Context.SelectedCommand -}}
  89. {{if .Context.SelectedCommand.Commands -}}
  90. {{T "Commands:"}}
  91. {{.Context.SelectedCommand}}
  92. {{.Context.SelectedCommand.Commands|CommandsToTwoColumns|FormatTwoColumns}}
  93. {{end -}}
  94. {{else if .App.Commands -}}
  95. {{T "Commands:"}}
  96. {{.App.Commands|CommandsToTwoColumns|FormatTwoColumns}}
  97. {{end -}}
  98. `
  99. type rateFlagValue struct {
  100. infinity bool
  101. limit rate.Limit
  102. v string
  103. }
  104. func (f *rateFlagValue) Set(v string) error {
  105. if v == "infinity" {
  106. f.infinity = true
  107. return nil
  108. }
  109. retErr := fmt.Errorf("--rate format %q doesn't match the \"freq/duration\" (i.e. 50/1s)", v)
  110. ps := strings.SplitN(v, "/", 2)
  111. switch len(ps) {
  112. case 1:
  113. ps = append(ps, "1s")
  114. case 0:
  115. return retErr
  116. }
  117. freq, err := strconv.Atoi(ps[0])
  118. if err != nil {
  119. return retErr
  120. }
  121. if freq == 0 {
  122. f.infinity = true
  123. return nil
  124. }
  125. switch ps[1] {
  126. case "ns", "us", "µs", "ms", "s", "m", "h":
  127. ps[1] = "1" + ps[1]
  128. }
  129. per, err := time.ParseDuration(ps[1])
  130. if err != nil {
  131. return retErr
  132. }
  133. f.limit = rate.Limit(float64(freq) / per.Seconds())
  134. f.v = v
  135. return nil
  136. }
  137. func (f *rateFlagValue) Limit() *rate.Limit {
  138. if f.infinity {
  139. return nil
  140. }
  141. return &f.limit
  142. }
  143. func (f *rateFlagValue) String() string {
  144. return f.v
  145. }
  146. func rateFlag(c *kingpin.Clause) (target *rateFlagValue) {
  147. target = new(rateFlagValue)
  148. c.SetValue(target)
  149. return
  150. }
  151. func main() {
  152. kingpin.UsageTemplate(CompactUsageTemplate).
  153. Version(version).
  154. Author("six-ddc@github").
  155. Resolver(kingpin.PrefixedEnvarResolver("PLOW_", ";")).
  156. Help = `A high-performance HTTP benchmarking tool with real-time web UI and terminal displaying`
  157. kingpin.Parse()
  158. if *requests >= 0 && *requests < int64(*concurrency) {
  159. errAndExit("requests must greater than or equal concurrency")
  160. return
  161. }
  162. if (*cert != "" && *key == "") || (*cert == "" && *key != "") {
  163. errAndExit("must specify cert and key at the same time")
  164. return
  165. }
  166. if *pprofAddr != "" {
  167. go http.ListenAndServe(*pprofAddr, nil)
  168. }
  169. var err error
  170. var bodyBytes []byte
  171. var bodyFile string
  172. if *body != "" {
  173. if strings.HasPrefix(*body, "@") {
  174. fileName := (*body)[1:]
  175. if _, err = os.Stat(fileName); err != nil {
  176. errAndExit(err.Error())
  177. return
  178. }
  179. if *stream {
  180. bodyFile = fileName
  181. } else {
  182. bodyBytes, err = os.ReadFile(fileName)
  183. if err != nil {
  184. errAndExit(err.Error())
  185. return
  186. }
  187. }
  188. } else {
  189. bodyBytes = []byte(*body)
  190. }
  191. if !methodSet {
  192. *method = "POST"
  193. }
  194. }
  195. clientOpt := ClientOpt{
  196. url: *url,
  197. method: *method,
  198. headers: *headers,
  199. bodyBytes: bodyBytes,
  200. bodyFile: bodyFile,
  201. certPath: *cert,
  202. keyPath: *key,
  203. insecure: *insecure,
  204. maxConns: *concurrency,
  205. doTimeout: *timeout,
  206. readTimeout: *respReadTimeout,
  207. writeTimeout: *reqWriteTimeout,
  208. dialTimeout: *dialTimeout,
  209. socks5Proxy: *socks5,
  210. contentType: *contentType,
  211. host: *host,
  212. }
  213. requester, err := NewRequester(*concurrency, *requests, *duration, reqRate.Limit(), &clientOpt)
  214. if err != nil {
  215. errAndExit(err.Error())
  216. return
  217. }
  218. // description
  219. var desc string
  220. desc = fmt.Sprintf("Benchmarking %s", *url)
  221. if *requests > 0 {
  222. desc += fmt.Sprintf(" with %d request(s)", *requests)
  223. }
  224. if *duration > 0 {
  225. desc += fmt.Sprintf(" for %s", duration.String())
  226. }
  227. desc += fmt.Sprintf(" using %d connection(s).", *concurrency)
  228. fmt.Fprintln(os.Stderr, desc)
  229. // charts listener
  230. var ln net.Listener
  231. if *chartsListenAddr != "" {
  232. ln, err = net.Listen("tcp", *chartsListenAddr)
  233. if err != nil {
  234. errAndExit(err.Error())
  235. return
  236. }
  237. fmt.Fprintf(os.Stderr, "@ Real-time charts is listening on http://%s\n", ln.Addr().String())
  238. }
  239. fmt.Fprintln(os.Stderr, "")
  240. // do request
  241. go requester.Run()
  242. // metrics collection
  243. report := NewStreamReport()
  244. go report.Collect(requester.RecordChan())
  245. if ln != nil {
  246. // serve charts data
  247. charts, err := NewCharts(ln, report.Charts, desc)
  248. if err != nil {
  249. errAndExit(err.Error())
  250. return
  251. }
  252. go charts.Serve(*autoOpenBrowser)
  253. }
  254. // terminal printer
  255. printer := NewPrinter(*requests, *duration, !*clean, *summary)
  256. printer.PrintLoop(report.Snapshot, *interval, *seconds, *jsonFormat, report.Done())
  257. }