diff --git a/README.md b/README.md index 93cea0f0..ea222c22 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ Options: "csv" outputs the response metrics in comma-separated values format. "json" outputs the metrics report in JSON format. "pretty" outputs the metrics report in pretty JSON format. + "html" outputs the metrics report as HTML. -i Comma separated list of proto import paths. The current working directory and the directory of the protocol buffer file are automatically added to the import list. @@ -186,6 +187,10 @@ duration (ms),status,error ... ``` +HTML output can be generated using `html` as format in the `-O` option. See [sample output](http://bojand.github.io/sample.html). + +Using `-O json` outputs JSON data, and `-O pretty` outputs JSON in pretty format. + ## Credit Icon made by Freepik from www.flaticon.com is licensed by CC 3.0 BY diff --git a/cmd/grpcannon/main.go b/cmd/grpcannon/main.go index 809e5f02..e68d2c23 100644 --- a/cmd/grpcannon/main.go +++ b/cmd/grpcannon/main.go @@ -85,6 +85,7 @@ Options: "csv" outputs the response metrics in comma-separated values format. "json" outputs the metrics report in JSON format. "pretty" outputs the metrics report in pretty JSON format. + "html" outputs the metrics report as HTML. -i Comma separated list of proto import paths. The current working directory and the directory of the protocol buffer file are automatically added to the import list. diff --git a/printer/printer.go b/printer/printer.go index e9d58056..1fe37866 100644 --- a/printer/printer.go +++ b/printer/printer.go @@ -60,6 +60,15 @@ func (rp *ReportPrinter) Print(format string) { } rp.printf(string(rep)) + case "html": + buf := &bytes.Buffer{} + templ := template.Must(template.New("tmpl").Funcs(tmplFuncMap).Parse(htmlTmpl)) + if err := templ.Execute(buf, *rp.Report); err != nil { + log.Println("error:", err.Error()) + return + } + + rp.printf(buf.String()) } } @@ -72,6 +81,8 @@ var tmplFuncMap = template.FuncMap{ "formatSeconds": formatSeconds, "histogram": histogram, "jsonify": jsonify, + "formatMark": formatMarkMs, + "formatPercent": formatPercent, } func jsonify(v interface{}) string { @@ -87,6 +98,11 @@ func formatSeconds(duration float64) string { return fmt.Sprintf("%4.2f", duration) } +func formatPercent(num int, total uint64) string { + p := float64(num) / float64(total) + return fmt.Sprintf("%.2f", p*100) +} + func histogram(buckets []grpcannon.Bucket) string { max := 0 for _, b := range buckets { @@ -106,6 +122,10 @@ func histogram(buckets []grpcannon.Bucket) string { return res.String() } +func formatMarkMs(m float64) string { + return fmt.Sprintf("'%4.3f ms'", m*1000) +} + var ( defaultTmpl = ` Summary: @@ -129,5 +149,355 @@ Status code distribution:{{ range $code, $num := .StatusCodeDist }} csvTmpl = ` duration (ms),status,error{{ range $i, $v := .Details }} {{ formatMilli .Latency.Seconds }},{{ .Status }},{{ .Error }}{{ end }} +` + + htmlTmpl = ` + + + + + + Results + + + + + + + + + + + +
+ +
+ +
+
+ +
+
+
+
+ +

Summary

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Count{{ .Count }}
Total{{ formatMilli .Total.Seconds }} ms
Slowest{{ formatMilli .Slowest.Seconds }} ms
Fastest{{ formatMilli .Fastest.Seconds }} ms
Average{{ formatMilli .Average.Seconds }} ms
Requests / sec{{ formatSeconds .Rps }}
+
+
+
+
+ +
+
+
+ +

Historam

+
+

+

+

+
+
+ +
+
+
+ +

Latency distribution

+
+ + + + {{ range .LatencyDistribution }} + + {{ end }} + + + + + {{ range .LatencyDistribution }} + + {{ end }} + + +
{{ .Percentage }} %%
{{ formatMilli .Latency.Seconds }} ms
+
+
+ +
+
+
+
+
+ +

Status distribution

+
+ + + + + + + + + + {{ range $code, $num := .StatusCodeDist }} + + + + + + {{ end }} + +
StatusCount%% of Total
{{ $code }}{{ $num }}{{ formatPercent $num $.Count }} %%
+
+
+
+
+ + {{ if gt (len .ErrorDist) 0 }} + +
+
+
+
+
+ +

Errors

+
+ + + + + + + + + + {{ range $err, $num := .ErrorDist }} + + + + + + {{ end }} + +
ErrorCount%% of Total
{{ $err }}{{ $num }}{{ formatPercent $num $.Count }} %%
+
+
+
+
+ + {{ end }} + +
+
+
+
+
+ +

Data

+
+ + JSON + CSV +
+
+
+
+ +
+
+
+

+ Generated by ghz +

+ +
+
+ +
+ + + + + + + + ` ) diff --git a/testdata/localhost.crt b/testdata/localhost.crt index 478d2248..bb1d4fc8 100644 --- a/testdata/localhost.crt +++ b/testdata/localhost.crt @@ -1,18 +1,18 @@ -----BEGIN CERTIFICATE----- -MIIC5TCCAc2gAwIBAgIJAJFwlruqLNgaMA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNV -BAMMCWxvY2FsaG9zdDAeFw0xODA2MDExNjA1NDNaFw0xODA3MDExNjA1NDNaMBQx +MIIC5TCCAc2gAwIBAgIJAPMeFjozN41RMA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNV +BAMMCWxvY2FsaG9zdDAeFw0xODA3MTAyMzMyNTNaFw0xODA4MDkyMzMyNTNaMBQx EjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC -ggEBALQ1gPZBLD2D4HBveN4eRzal1r8lX4AQSq9h4ofcMo/0sqz7yEQ3zH41Ff+U -KyeBSpXQ6eHtQNiJAATFNPzmHtCbS/YsE18+Msab+Qz0Bgk3pCM97k7japl6Hzag -83C/c2U1Wr3i5otCUQhk+pY2zWjh07gRZWmkp10Ijapxf7mQwC3u2vguzfXEE9co -BS2wyRiJxbwN3DIhW7fTZfoOgfnEGktrTF+hlO8YPNfodZum0jwrlv0hAwYfDiIJ -yQmNMbBLAcZIeH/ZKXEKQkFFKYfxYVMM9Hf4B2XOJ8z7M5o0R+l059RHqric6/c1 -9XPvfDvWSIijI5HgymNAUZmFT+ECAwEAAaM6MDgwFAYDVR0RBA0wC4IJbG9jYWxo +ggEBALVtHZE+p5r4QlS9jib5AHHlt3Yyr+ASKhkNUX+tNHZSjy1kkrQmuyFMA71m +Hn0D0ASBG7kEqfPnmLY+VSurte43LZOgEWH0tUm4cCSfwKaaHG3yr5RPwc/rhI3U +TIgSZWpitafzYbaqU1dEsBbHiYPOnrZmRYmXefsPE3Bob4/c9lf+V1UjGE/v1AWa +HCaJPId1xn22rRSQeoZS6zNx1ICi2b2Q2S1Geb/a24E06OgaoJExMSfNJP04oECt +uVu1pnNdpfBA8exRB2JCyxVnRZnK+wmaADmsrvpP7b7731dmdNy7ztb3wXbXewpa +TY13ZEfSuLFrd8lsB0kXfJYip3cCAwEAAaM6MDgwFAYDVR0RBA0wC4IJbG9jYWxo b3N0MAsGA1UdDwQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDATANBgkqhkiG9w0B -AQsFAAOCAQEAK3MQsX8AN9x1FeOV79Qitk9Hzy0UoGDGcfn/so/2cAbU1mY1Uk6h -W9iCLiBKrKkoO8Ys6hR5Rx5f809pbe7hKmyQ4C0WdGxwUydAPloKj4kvf8oWcPRd -SYxuaeD7I/tGuet7A5krneUKJi2bZtB/8OiDg64PRHNbK3wU25yB9t14aQbeXEzF -qDlId9HZVZLVOb0G/XhcSX5kHiVVrk97x3hXsMh8iAt7zjbn2y8QitKsTpGpfsZ2 -j2a8OoV/shKt20Ea7zGMc/U/UDp3auKnD1RMA1Z11sht5cW+l8YTJ7UVLfZEdlnP -XQSacAbjHwZ/YxEdSCiKTc+8wHGTb28aeQ== +AQsFAAOCAQEASY0u4HrIvk56+fz0zfl4921ZrASDmW4a9vyJYfBYSYJetk6HO9bV +B47JzzV32VipO5u5KXesU5BJ+RsJMakIE7viimhKD17HJGHFAtws520z/gDixHNP +bJiNc71On9GvMOJANKqvYvTRvquJT6XA4TAHpbHyB1kZjQRk+NwSWuzd1/Cbanl/ ++D5z0dBboVQ893gAjQZmZdsjenSjlfV6iXdkrbFpp0SxKnRzSr8z09HOJ4n95QbG +WYk/pHffr28KBAznaO/rTKH/9EmZ62RXXrtSrLcOEQxG3rXpufIvR0sJudQH97gU +GRgmIuMq+utwhFHP1+rZj7Kwh+u+anN4Lg== -----END CERTIFICATE----- diff --git a/testdata/localhost.key b/testdata/localhost.key index 898e7cd7..ee8aeae4 100644 --- a/testdata/localhost.key +++ b/testdata/localhost.key @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC0NYD2QSw9g+Bw -b3jeHkc2pda/JV+AEEqvYeKH3DKP9LKs+8hEN8x+NRX/lCsngUqV0Onh7UDYiQAE -xTT85h7Qm0v2LBNfPjLGm/kM9AYJN6QjPe5O42qZeh82oPNwv3NlNVq94uaLQlEI -ZPqWNs1o4dO4EWVppKddCI2qcX+5kMAt7tr4Ls31xBPXKAUtsMkYicW8DdwyIVu3 -02X6DoH5xBpLa0xfoZTvGDzX6HWbptI8K5b9IQMGHw4iCckJjTGwSwHGSHh/2Slx -CkJBRSmH8WFTDPR3+AdlzifM+zOaNEfpdOfUR6q4nOv3NfVz73w71kiIoyOR4Mpj -QFGZhU/hAgMBAAECggEBAJFEqCbatq0IB/7a/VYglkuJOClyGSAFAg+LGq9mZCQD -n50ugmvrhx8d8BPM/1SjNtq6RC9pr+Jd3fP6fRJ49tI2ve897IfUfd34kbVNaSg5 -AEmgfOB/FsmN1meVK2jyDDXD4tg7dpk/5k6cCBzbJI6trJwu+c9FedzXLkv9nnaY -zNKkVfRFFgS9f0e48ASDSDERYzbPEsO6OXkZHZIFfXcE0I297LtwzeYQN0ScGQ98 -3CC/lAipClpMsENfhEH5SIbLT6I9IXSzT+yDiA+Q6PApzuXQd0d+Y3ADns7rn8Ie -QTGNRMOhFKSqciOdW/fL+gHlyoqcWU5XCP2wfTVnrqECgYEA26HzsdHgRKnBe4Cf -GwEw6t+nA2Mf04AXpfJDVy5UEMupiPOZjZi/jPO539BKl1sks/nYT2oFfL6f+1PP -vH1G/NMTJVRumWrmY+tu/VzdTd7cQTWBvbbfvRlQ7Jc84przUEyFRdmXhydgoHf1 -la/4D+IebZAZTeSwK2jdePIWZiUCgYEA0gxqnBPoR8EfxqXKYb/waV7b5XBwnmo9 -IIu9mEEHU7S1CEie67+T69Lx6NM6Sjx8mI679oYWMHbuWR3pOVgn4SYeQRLIprKd -ceUlyVIbvSWEbUBoNnQ7xp5x2/WpT8vtf30AHcsEOIMFxsqkzwmKshdBTqfpK4Up -m42wborvoA0CgYBYKJ+q5rWAmishqbUzn7zE6lUdlPI3cRkM8Tt6iQwRWc6JPE6M -eZ2ZtFMNtYvbSShoXYcoCUR+l/2bYj7mR9rwrMDooQVr627i+KOqa1YhZa0/N30G -a5tPShQjg2lbBBtaRfzQ1tBt3a55eu1G0kVeCsNv8wFVNNBJ/GO5omK/SQKBgCIS -n4yX8hsJqeToae73WsFNAPC0D6Cy7R2FbYjwK4cZjjA8z4LAffdILbOt6Au4yiFZ -LgZsc9cCw+Ey5+1EbpuoOkomCOR5nu6l1D5XEmbZWiT6yKzkp/mtJB0hOYjXNLx0 -g3tRvmqIXnyDzL5E9vmyqgZfWISVwk0Ya+FSqlJVAoGBAKY925otoT53TlFyT/6l -rZVeAfne5OR1HbcT+wsm4QMCKoewZaT9r8RCHMvQfnucJggbLxdY0/+WpN7v9nlg -QHVuJ9g2jJ7mHXXq31R1o6G/c0mmNKXqPmsPBWbkNF2MIfJmKh2FmsBREdt9xZ4V -uRge7upZZNUDy1HbZhAX8+du +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC1bR2RPqea+EJU +vY4m+QBx5bd2Mq/gEioZDVF/rTR2Uo8tZJK0JrshTAO9Zh59A9AEgRu5BKnz55i2 +PlUrq7XuNy2ToBFh9LVJuHAkn8Cmmhxt8q+UT8HP64SN1EyIEmVqYrWn82G2qlNX +RLAWx4mDzp62ZkWJl3n7DxNwaG+P3PZX/ldVIxhP79QFmhwmiTyHdcZ9tq0UkHqG +UuszcdSAotm9kNktRnm/2tuBNOjoGqCRMTEnzST9OKBArblbtaZzXaXwQPHsUQdi +QssVZ0WZyvsJmgA5rK76T+2++99XZnTcu87W98F213sKWk2Nd2RH0rixa3fJbAdJ +F3yWIqd3AgMBAAECggEBAIS+LIcMD7fcczPraWnsgD3VtRE/bt2EjNa9nubLBPqZ +13aAArLbL8niA059vEr+EiJpkK4j0nHJ/ztAijKktT1xk+BSmsVQNaCV2T+Cv4Nm +eOydR4g9fvIxQVBAiCp7Q0a+qGkVM2ZiYw4UDL7uChitggS0+aX9dduSoUd6sj3Q +Uk6CT4BUIjB1+yAhLY53NgGqX/iAkbt84+JWug3ELZ3lrcSOe4NdBEsN1PNrbKiV +BzQ4dQBJF9O8pz10HYV/gKoLHxmbII067ERyfDmliTJONy/xUx1zxF37gMg32yTz +BsILDmwGB3unTAjB8W6nNkEnLMmlkZs84R/Ztrr9S2ECgYEA7q3qvSSwY+fCef2q +geM2LCP2aCuOHnBLCbiXu1tdQH6tg3fDDVvhpON3aIaAHTCKJzB96xdC1tWaqG9f +zxvEHx+n7VZQvmdOoS2t3aDPbIgxszdnPd7K5KqHx5JdM5R8X6/ge0TwN+QRoHxO +an9jH/7CLcd49SBNSDADKdJWp3ECgYEAwpeTC1oehR+d6VWDHsSlparDTnBPGhng +QLB18jSGiE38g50bkNt0Y/ou/X+ASdXIlfyonGegQJ8xTBb33rhWl0a/mNpgTd6x +bFqMEoAkaIMT5ismfYiwe3QtUKKXxBX+4PufnBN14yjpS1Ng8CXZbbJHcRB9zaD8 +jhLHzgR/WWcCgYBP+tWDRi0ZfUsM5/TgC4xWpEJoy9eW2Zg7jLDDpq0L9KceO96P +tm9ZeqPD0fZSUbapxTUctzG9ndxyfsfDNhG1QD1caaUq+KE/n3f4oW+Ade75mCIv +eF3S3FYdiS1UIHjA88WsaQB6KiHB/oWkaEWGLi9RedtNgOKEwxn8RLCm8QKBgQC/ +q59aGi1fwRHA6MrbfqUO9pGj7Hnt4rkB3w5+QQCWuAJW/WROnesaIy8v4OvTTnFi +U1kdnfC6VlOveBw00+vu2/ATNl6PtR+b8+BuxKuYCiyKlcw28HGtWHTeLRlCOsww +fYjsqjp/QyfwWzNruExfgbBfDPhtl/nMU3FdWw65VwKBgQCkG6RiRVOOlxXgIIhb +emDTh6K8Y6OJ7RNNA0rdPM4iUmkJrPSIsIhYEYUfNthiw8Wj17ISQ1mtKvNzTOZZ +MO6WJ1zczNjVBwSiBnHiYzkMa/BeTpu97kKc5WJw4L4Z5QMm+t4y2gj3Zyqwgm4i +wzX6PLku248uTLUqwkXQvRAxxg== -----END PRIVATE KEY-----