1
0

main.go 9.0 KB

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