<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>PSRT stats</title>
<script src="chart.min.js"></script>
<style type="text/css">
.online { color: #57CA3B; }
.offline { color: #FF1616; }
.unavailable { color: #FF1616; }
.waiting { color: #636363; }
.connecting { color: orange; }
* { box-sizing: border-box; }
body { margin: 0; font-size: 15px; line-height: 18px; font-family: Arial, sans-serif; background: #2e2e2e; color: #fff; }
.container { max-width: 1150px; margin: auto; padding: 12px; }
h1 { margin: 0 0 32px; font-size: 32px; line-height: 32px; padding-top: 16px; padding-left: 60px; color: #FF1616;
background: url("data:image/svg+xml,%3Csvg width='80' height='80' viewBox='0 0 80 80' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M72.0291 71.1256C72.9098 70.2472 72.9184 68.8275 72.0482 67.9547L51.4483 47.291L73.4306 47.2329C74.6686 47.2272 75.6769 46.2188 75.6826 44.9804C75.6882 43.7421 74.6892 42.7429 73.4512 42.7486L46.0679 42.8314C45.4734 42.8342 44.9021 43.073 44.4798 43.4955C44.0575 43.9179 43.8187 44.4893 43.8159 45.084L43.7303 73.0641C43.7246 74.3025 44.7237 75.3017 45.9617 75.296C47.1998 75.2903 48.208 74.2818 48.2137 73.0435L48.2743 50.4873L68.8589 71.1357C69.7291 72.0086 71.1484 72.004 72.0291 71.1256Z' fill='white'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M7.56561 71.1256C6.68491 70.2472 6.67636 68.8275 7.54651 67.9547L28.1464 47.291L6.16417 47.2329C4.92613 47.2272 3.91787 46.2188 3.91217 44.9804C3.90648 43.7421 4.90549 42.7429 6.14354 42.7486L33.5268 42.8314C34.1213 42.8342 34.6926 43.073 35.1149 43.4955C35.5373 43.9179 35.7761 44.4893 35.7788 45.084L35.8644 73.0641C35.8701 74.3025 34.8711 75.3017 33.633 75.296C32.395 75.2903 31.3867 74.2818 31.381 73.0435L31.3204 50.4873L10.7358 71.1357C9.86564 72.0086 8.4463 72.004 7.56561 71.1256Z' fill='white'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M45.7472 33.9874C44.8665 33.109 44.858 31.6893 45.7282 30.8165L66.328 10.1528L44.3458 10.0948C43.1078 10.0891 42.0995 9.08058 42.0938 7.84226C42.0881 6.60393 43.0871 5.60469 44.3252 5.61038L71.7084 5.69327C72.303 5.696 72.8742 5.93486 73.2966 6.35729C73.7189 6.77971 73.9577 7.3511 73.9604 7.94577L74.046 35.9259C74.0517 37.1643 73.0527 38.1635 71.8147 38.1578C70.5766 38.1521 69.5684 37.1436 69.5627 35.9053L69.502 13.3491L48.9174 33.9975C48.0473 34.8704 46.6279 34.8659 45.7472 33.9874Z' fill='%23FF1616'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M7.56561 6.65905C6.68491 7.53747 6.67636 8.95715 7.54651 9.82999L28.1464 30.4937L6.16417 30.5517C4.92613 30.5574 3.91787 31.5659 3.91217 32.8042C3.90648 34.0426 4.90549 35.0418 6.14354 35.0361L33.5268 34.9532C34.1213 34.9505 34.6926 34.7116 35.1149 34.2892C35.5373 33.8668 35.7761 33.2954 35.7788 32.7007L35.8644 4.72054C35.8701 3.48222 34.8711 2.48298 33.633 2.48867C32.395 2.49437 31.3867 3.50285 31.381 4.74118L31.3204 27.2973L10.7358 6.64896C9.86564 5.77612 8.4463 5.78063 7.56561 6.65905Z' fill='white'/%3E%3Cpath d='M43.1875 38C43.1875 39.7604 41.7604 41.1875 40 41.1875C38.2396 41.1875 36.8125 39.7604 36.8125 38C36.8125 36.2396 38.2396 34.8125 40 34.8125C41.7604 34.8125 43.1875 36.2396 43.1875 38Z' fill='white' stroke='white' stroke-width='3'/%3E%3C/svg%3E%0A") 0 0 no-repeat;
background-size: 50px;
}
h2 { font-size: 28px; font-weight: 500; line-height: 34px; color: #f1f1f1; margin: 0 0 12px; }
.bold { font-weight: bold; }
.content { display: flex; flex-wrap: wrap; }
.col-1, .col-2 { width: 100%; }
.status { padding: 8px; line-height: 24px; background: #222; }
.col-1 > div, .col-2 > div { margin-bottom: 20px; }
.status div:not(:last-child) { margin-bottom: 8px; }
.counters { width: 100%; overflow-x: auto; }
table { width: 100%; background: #222; border: none; border-collapse: collapse; font-size: 16px;}
td { border: 1px solid #545454; padding: 10.5px 6px; }
td span { display: inline-block; min-width: 100px; }
.data-cell { text-align: right; }
.color-grey { color: #636363; font-size: 12px; }
.copyright { font-size: 14px; }
a { color: #fff; }
a:visited { color: #fff; }
a:hover { color: orange; }
.charts { display: flex; flex-wrap: wrap; justify-content: space-between; }
.chart { width: 100%; border: 0.5px solid #545454; border-radius: 4px; box-shadow: 2px 4px 6px rgba(30, 30, 31, 0.34); background: #222; margin-bottom: 12px; }
@media screen and (min-width: 768px) {
.container { padding: 20px; }
.col-1 { width: 60%; }
.col-2 { width: 40%; padding-left: 20px; }
.chart { width: calc(50% - 5px) }
}
@media screen and (min-width: 992px) {
.col-2 { padding-left: 50px; }
.chart { width: calc(33.33% - 6px); }
}
</style>
</head>
<script type="text/javascript">
var chart_sub_ops;
var chart_sent_latency;
var chart_pub_messages;
var chart_pub_bytes;
var chart_sent_messages;
var chart_sent_bytes;
function format_bytes(bytes) {
if (bytes === 0) return "0 B";
if (bytes < 1) return "";
const k = 1024;
const sizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
}
function create_chart(counter, units, color) {
let cnt = document.getElementById("ch_" + counter);
let ctx = document.createElement('canvas');
cnt.append(ctx);
let labels = [];
let data = [];
for (let i=0; i<60; i++) {
labels.push(i);
data.push(0);
}
let ytickcfg = {};
ytickcfg.color = "#bdbdbd";
if (units == 'bytes/sec') {
ytickcfg.callback = function(value, index, values) { return format_bytes(value); }
}
return new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [{ label: `c_${counter}, ${units}`,
data: data, backgroundColor: [ `rgba(${color},0.5)`,
], borderColor: [ `rgba(${color}, 0.8)`, ], borderWidth: 1 }]
},
options: { animation: false,
scales: {
y: { grid: { color: "#333333" }, beginAtZero: true, ticks: ytickcfg },
x: { grid: { display: false }, ticks: { display: false } }
}
}
});
}
function update_chart(chart, value) {
let cdata = chart.data.datasets[0].data;
cdata.shift();
cdata.push(value);
chart.update();
}
function start() {
chart_sub_ops = create_chart("sub_ops", "ops/sec", "155,52,235");
chart_sent_latency = create_chart("sent_latency", "µs avg", "138,50,86");
chart_pub_messages = create_chart("pub_messages", "ops/sec", "60,133,91");
chart_pub_bytes = create_chart("pub_bytes", "bytes/sec", "29,153,219");
chart_sent_messages = create_chart("sent_messages", "ops/sec", "201,195,16");
chart_sent_bytes = create_chart("sent_bytes", "bytes/sec", "245,149,66");
update_status();
}
var data_prev;
var u64_max = 0xffff_ffff_ffff_ffff;
function fnum(x) {
return x.toString().replace(/\B(?<!\.\d*)(?=(\d{3})+(?!\d))/g, "_");
}
function set_el(el_id, value, format_number) {
let el = document.getElementById(el_id);
if (format_number) {
el.innerHTML = fnum(value);
} else {
el.innerHTML = value;
}
}
function cnt_delta(val, prev_val) {
if (val >= prev_val) {
return val - prev_val
} else {
return val + (u64_max - prev_val);
}
}
function cnt_speed(val, prev_val, time) {
if (!time) {
return 0;
}
let speed = cnt_delta(val, prev_val) / time;
return Math.round(speed);
}
function update_status() {
fetch('status', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
if (!data_prev || data.time > data_prev.time) {
set_el("status", "<span class='online'>online</span>");
set_el("host", data.host);
let time = String(new Date(data.time / 1_000));
let ti = time.indexOf('(');
if (ti) {
time = time.substring(0, ti);
}
set_el("time", time);
set_el("uptime", data.uptime, true);
set_el("data_queue_size", data.data_queue_size, true);
set_el("version", data.version);
set_el("c_sub_ops", data.counters.c_sub_ops, true);
set_el("c_pub_messages", data.counters.c_pub_messages, true);
set_el("c_pub_bytes", data.counters.c_pub_bytes, true);
set_el("c_sent_messages", data.counters.c_sent_messages, true);
set_el("c_sent_bytes", data.counters.c_sent_bytes, true);
set_el("c_sent_latency", data.counters.c_sent_latency, true);
set_el("client_count", data.clients.client_count, true);
set_el("subscription_count", data.clients.subscription_count, true);
if (data_prev) {
let time_delta = (data.time - data_prev.time) / 1_000_000;
let sub_ops_speed = cnt_speed(data.counters.c_sub_ops, data_prev.counters.c_sub_ops, time_delta);
set_el("sub_ops_speed", sub_ops_speed, true);
update_chart(chart_sub_ops, sub_ops_speed);
let pub_speed = cnt_speed(data.counters.c_pub_messages, data_prev.counters.c_pub_messages, time_delta);
set_el("pub_speed", pub_speed, true);
update_chart(chart_pub_messages, pub_speed);
let pub_bytes_speed = cnt_speed(data.counters.c_pub_bytes, data_prev.counters.c_pub_bytes, time_delta);
set_el("pub_bytes_speed", format_bytes(pub_bytes_speed), false);
update_chart(chart_pub_bytes, pub_bytes_speed);
let sent_speed = cnt_speed(data.counters.c_sent_messages, data_prev.counters.c_sent_messages, time_delta);
set_el("sent_speed", sent_speed, true);
update_chart(chart_sent_messages, sent_speed);
let sent_bytes_speed = cnt_speed(data.counters.c_sent_bytes, data_prev.counters.c_sent_bytes, time_delta);
set_el("sent_bytes_speed", format_bytes(sent_bytes_speed), false);
update_chart(chart_sent_bytes, sent_bytes_speed);
let msg_cnt = cnt_delta(data.counters.c_sent_messages, data_prev.counters.c_sent_messages);
let latency_avg = 0;
if (msg_cnt) {
latency_avg = Math.round(cnt_delta(data.counters.c_sent_latency,
data_prev.counters.c_sent_latency) / msg_cnt);
}
set_el("latency_avg", latency_avg, true);
update_chart(chart_sent_latency, latency_avg);
}
data_prev = data;
if (data.cluster == null) {
set_el("cluster", "<span style='color: #636363'>not configured</span>");
} else {
let cluster_table = "<table><tr style='font-weight: bold; color: #959595'><td>node</td><td>status</td></tr>";
for (let [key, val] of Object.entries(data.cluster)) {
cluster_table += `<tr><td>${key}</td><td><span class="${val}">${val}</span></td></tr>`;
}
cluster_table += "</table>";
set_el("cluster", cluster_table);
}
}
setTimeout(update_status, 1000);
})
.catch((error) => {
set_el("status", "<span class='offline'>offline</span>");
data_prev = null;
console.error('Error:', error);
setTimeout(update_status, 1000);
});
}
</script>
<body onload="start()">
<div class="container">
<h1>PSRT</h1>
<div class="content">
<div class="col-1">
<h2>Server info</h2>
<div class="status">
<div>host: <span id="host"></span></div>
<div>status: <span id="status"><span class="connecting">connecting...</span></span></div>
<div>uptime: <span id="uptime"></span><span> sec</span></div>
<div>data queue size: <span id="data_queue_size"></span></div>
<div>version: <span id="version"></span></div>
<div>server time: <span id="time"></span></div>
</div>
<div class="counters">
<h2>Cyclic counters</h2>
<table>
<tr>
<td>c_sub_ops</td>
<td class="data-cell"><span id="c_sub_ops" class="color-grey"></span></td>
<td class="data-cell"><span id="sub_ops_speed"></span> ops/sec</td>
<td>Subscribe / unsubscribe ops</td></tr>
<tr>
<td>c_pub_messages</td>
<td class="data-cell"><span id="c_pub_messages" class="color-grey"></span></td>
<td class="data-cell"><span id="pub_speed"></span> ops/sec</td>
<td>Messages published</td>
</tr>
<tr>
<td>c_pub_bytes</td>
<td class="data-cell"><span id="c_pub_bytes" class="color-grey"></span></td>
<td class="data-cell"><span id="pub_bytes_speed"></span> bytes/sec</td>
<td>Bytes published</td>
</tr>
<tr>
<td>c_sent_messages</td>
<td class="data-cell"><span id="c_sent_messages" class="color-grey"></span></td>
<td class="data-cell"><span id="sent_speed"></span> ops/sec</td>
<td>Messages sent</td>
</tr>
<tr>
<td>c_sent_bytes</td>
<td class="data-cell"><span id="c_sent_bytes" class="color-grey"></span></td>
<td class="data-cell"><span id="sent_bytes_speed"></span> bytes/sec</td>
<td>Bytes sent</td>
</tr>
<tr>
<td>c_sent_latency</td>
<td class="data-cell"><span id="c_sent_latency" class="color-grey"></span></td>
<td class="data-cell"><span id="latency_avg"></span> µs, avg</td>
<td>Latency</td>
</tr>
</table>
</div>
</div>
<div class="col-2">
<div>
<h2>Clients</h2>
<table>
<tr>
<td>connected</td>
<td><span id="client_count"></span></td>
</tr>
<tr>
<td>subscriptions</td>
<td><span id="subscription_count"></span></td>
</tr>
</table>
</div>
<div>
<h2>Cluster</h2>
<div id="cluster"></div>
</div>
</div>
</div>
<div class="charts">
<div class="chart" id="ch_pub_messages"></div>
<div class="chart" id="ch_pub_bytes"></div>
<div class="chart" id="ch_sub_ops"></div>
<div class="chart" id="ch_sent_messages"></div>
<div class="chart" id="ch_sent_bytes"></div>
<div class="chart" id="ch_sent_latency"></div>
</div>
<div class="copyright">
<a href="https://github.com/alttch/psrt/">PSRT</a>
© 2022
<a href="https://www.bohemia-automation.com/">Bohemia Automation</a> /
<a href="https://www.altertech.com">Altertech</a>
</div>
</div>
</body>
</html>