Skip to content

Commit

Permalink
Showing 5 changed files with 416 additions and 40 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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 <a href="http://www.freepik.com" title="Freepik">Freepik</a> from <a href="https://www.flaticon.com/" title="Flaticon">www.flaticon.com</a> is licensed by <a href="http://creativecommons.org/licenses/by/3.0/" title="Creative Commons BY 3.0" target="_blank">CC 3.0 BY</a>
1 change: 1 addition & 0 deletions cmd/grpcannon/main.go
Original file line number Diff line number Diff line change
@@ -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.
370 changes: 370 additions & 0 deletions printer/printer.go
Original file line number Diff line number Diff line change
@@ -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 = `
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Results</title>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/papaparse@4.5.0/papaparse.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/britecharts@2/dist/bundled/britecharts.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/britecharts/dist/css/britecharts.min.css" type="text/css" /></head>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.1/css/bulma.min.css" />
</head>
<body>
<section class="section">
<div class="container">
<nav class="breadcrumb has-bullet-separator" aria-label="breadcrumbs">
<ul>
<li>
<a href="#summary">
<span class="icon is-small">
<i class="fas fa-clipboard-list" aria-hidden="true"></i>
</span>
<span>Summary</span>
</a>
</li>
<li>
<a href="#histogram">
<span class="icon is-small">
<i class="fas fa-chart-bar" aria-hidden="true"></i>
</span>
<span>Histogram</span>
</a>
</li>
<li>
<a href="#latency">
<span class="icon is-small">
<i class="far fa-clock" aria-hidden="true"></i>
</span>
<span>Latency Distribution</span>
</a>
</li>
<li>
<a href="#status">
<span class="icon is-small">
<i class="far fa-check-square" aria-hidden="true"></i>
</span>
<span>Status Distribution</span>
</a>
</li>
{{ if gt (len .ErrorDist) 0 }}
<li>
<a href="#errors">
<span class="icon is-small">
<i class="fas fa-exclamation-circle" aria-hidden="true"></i>
</span>
<span>Errors</span>
</a>
</li>
{{ end }}
<li>
<a href="#data">
<span class="icon is-small">
<i class="far fa-file-alt" aria-hidden="true"></i>
</span>
<span>Data</span>
</a>
</li>
</ul>
</nav>
<hr />
</div>
<div class="container">
<div class="columns">
<div class="column is-narrow">
<div class="content">
<a name="summary">
<h3>Summary</h3>
</a>
<table class="table">
<tbody>
<tr>
<th>Count</th>
<td>{{ .Count }}</td>
</tr>
<tr>
<th>Total</th>
<td>{{ formatMilli .Total.Seconds }} ms</td>
</tr>
<tr>
<th>Slowest</th>
<td>{{ formatMilli .Slowest.Seconds }} ms</td>
</tr>
<tr>
<th>Fastest</th>
<td>{{ formatMilli .Fastest.Seconds }} ms</td>
</tr>
<tr>
<th>Average</th>
<td>{{ formatMilli .Average.Seconds }} ms</td>
</tr>
<tr>
<th>Requests / sec</th>
<td>{{ formatSeconds .Rps }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<br />
<div class="container">
<div class="content">
<a name="historam">
<h3>Historam</h3>
</a>
<p>
<div class="js-bar-container"></div>
</p>
</div>
</div>
<br />
<div class="container">
<div class="content">
<a name="latency">
<h3>Latency distribution</h3>
</a>
<table class="table is-fullwidth">
<thead>
<tr>
{{ range .LatencyDistribution }}
<th>{{ .Percentage }} %%</th>
{{ end }}
</tr>
</thead>
<tbody>
<tr>
{{ range .LatencyDistribution }}
<td>{{ formatMilli .Latency.Seconds }} ms</td>
{{ end }}
</tr>
</tbody>
</table>
</div>
</div>
<br />
<div class="container">
<div class="columns">
<div class="column is-narrow">
<div class="content">
<a name="status">
<h3>Status distribution</h3>
</a>
<table class="table is-hoverable">
<thead>
<tr>
<th>Status</th>
<th>Count</th>
<th>%% of Total</th>
</tr>
</thead>
<tbody>
{{ range $code, $num := .StatusCodeDist }}
<tr>
<td>{{ $code }}</td>
<td>{{ $num }}</td>
<td>{{ formatPercent $num $.Count }} %%</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
</div>
</div>
</div>
{{ if gt (len .ErrorDist) 0 }}
<br />
<div class="container">
<div class="columns">
<div class="column is-narrow">
<div class="content">
<a name="errors">
<h3>Errors</h3>
</a>
<table class="table is-hoverable">
<thead>
<tr>
<th>Error</th>
<th>Count</th>
<th>%% of Total</th>
</tr>
</thead>
<tbody>
{{ range $err, $num := .ErrorDist }}
<tr>
<td>{{ $err }}</td>
<td>{{ $num }}</td>
<td>{{ formatPercent $num $.Count }} %%</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
</div>
</div>
</div>
{{ end }}
<br />
<div class="container">
<div class="columns">
<div class="column is-narrow">
<div class="content">
<a name="data">
<h3>Data</h3>
</a>
<a class="button" id="dlJSON">JSON</a>
<a class="button" id="dlCSV">CSV</a>
</div>
</div>
</div>
</div>
<div class="container">
<hr />
<div class="content has-text-centered">
<p>
Generated by <strong>ghz</strong>
</p>
<a href="https://github.com/bojand/grpcannon"><i class="icon is-medium fab fa-github"></i></a>
</div>
</div>
</section>
</body>
<script>
const count = {{ .Count }};
const rawData = {{ jsonify .Details }};
const data = [
{{ range .Histogram }}
{ name: {{ formatMark .Mark }}, value: {{ .Count }} },
{{ end }}
];
function createHorizontalBarChart() {
let barChart = britecharts.bar(),
tooltip = britecharts.miniTooltip(),
barContainer = d3.select('.js-bar-container'),
containerWidth = barContainer.node() ? barContainer.node().getBoundingClientRect().width : false,
tooltipContainer,
dataset;
tooltip.numberFormat('')
tooltip.valueFormatter(function(v) {
var percent = v / count * 100;
return v + ' ' + '(' + Number.parseFloat(percent).toFixed(1) + ' %%)';
})
if (containerWidth) {
dataset = data;
barChart
.isHorizontal(true)
.isAnimated(true)
.margin({
left: 100,
right: 20,
top: 20,
bottom: 20
})
.colorSchema(britecharts.colors.colorSchemas.teal)
.width(containerWidth)
.yAxisPaddingBetweenChart(20)
.height(400)
// .hasPercentage(true)
.enableLabels(true)
.labelsNumberFormat('')
.percentageAxisToMaxRatio(1.3)
.on('customMouseOver', tooltip.show)
.on('customMouseMove', tooltip.update)
.on('customMouseOut', tooltip.hide);
barChart.orderingFunction(function(a, b) {
var nA = a.name.replace(/ms/gi, '');
var nB = b.name.replace(/ms/gi, '');
var vA = Number.parseFloat(nA);
var vB = Number.parseFloat(nB);
return vB - vA;
})
barContainer.datum(dataset).call(barChart);
tooltipContainer = d3.select('.js-bar-container .bar-chart .metadata-group');
tooltipContainer.datum([]).call(tooltip);
}
}
function setJSONDownloadLink () {
var filename = "data.json";
var btn = document.getElementById('dlJSON');
var jsonData = JSON.stringify(rawData)
var blob = new Blob([jsonData], { type: 'text/json;charset=utf-8;' });
var url = URL.createObjectURL(blob);
btn.setAttribute("href", url);
btn.setAttribute("download", filename);
}
function setCSVDownloadLink () {
var filename = "data.csv";
var btn = document.getElementById('dlCSV');
var csv = Papa.unparse(rawData)
var blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
var url = URL.createObjectURL(blob);
btn.setAttribute("href", url);
btn.setAttribute("download", filename);
}
createHorizontalBarChart();
setJSONDownloadLink();
setCSVDownloadLink();
</script>
<script defer src="https://use.fontawesome.com/releases/v5.1.0/js/all.js"></script>
</html>
`
)
28 changes: 14 additions & 14 deletions testdata/localhost.crt
Original file line number Diff line number Diff line change
@@ -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-----
52 changes: 26 additions & 26 deletions testdata/localhost.key
Original file line number Diff line number Diff line change
@@ -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-----

0 comments on commit 198e664

Please sign in to comment.