ekg/assets/monitor.js
Johan Tibell 0f8352cff3 Break out core metric tracking into a new package
The new package, ekg-core, contains the metrics store, modules for
defining metrics, and functions for sampling the metrics.

The counter naming is also changed to include a namespace part. For
example "myapp.requests".

Finally the JSON format has been changed to be more self-descriptive.
2014-04-20 10:18:13 +02:00

383 lines
12 KiB
JavaScript

$(document).ready(function () {
"use strict";
// Number formatters
function commaify(n)
{
var nStr = n.toString();
var x = nStr.split('.');
var x1 = x[0];
var x2 = x.length > 1 ? '.' + x[1] : '';
var rgx = /(\d+)(\d{3})/;
while (rgx.test(x1)) {
x1 = x1.replace(rgx, '$1' + ',' + '$2');
}
return x1 + x2;
}
function formatSuffix(val, opt_prec) {
if (val === null) {
return "N/A";
}
var prec = opt_prec || 1;
if (val >= 1000000000) {
return (val / 1000000000).toFixed(prec) + " GB";
} else if (val >= 1000000) {
return (val / 1000000).toFixed(prec) + " MB";
} else if (val >= 1000) {
return (val / 1000).toFixed(prec) + " kB";
} else {
return val.toFixed(prec) + " B";
}
}
function formatRate(val, prec) {
if (val === null) {
return "N/A";
}
return formatSuffix(val, prec) + "/s";
}
function formatPercent(val, opt_prec) {
if (val === null) {
return "N/A";
}
var prec = opt_prec || 1;
return val.toFixed(prec) + " %";
}
// Set up polling interval control
var updateInterval = 1000; // ms
$("#updateInterval").val(updateInterval).change(function () {
updateInterval = $(this).val();
});
// Allow the UI to be paused
var paused = false;
$('#pause-ui').click(function () {
if (paused) {
$(this).text("Pause UI");
paused = false;
} else {
$(this).text("Unpause UI");
paused = true;
}
});
// Plot formatters
function suffixFormatter(val, axis) {
return formatSuffix(val, axis.tickDecimals);
}
function suffixFormatterGeneric(val, axis) {
if (val >= 1000000000) {
return (val / 1000000000).toFixed(axis.tickDecimals) + " G";
} else if (val >= 1000000) {
return (val / 1000000).toFixed(axis.tickDecimals) + " M";
} else if (val >= 1000) {
return (val / 1000).toFixed(axis.tickDecimals) + " k";
} else {
return val.toFixed(axis.tickDecimals);
}
}
function rateFormatter(val, axis) {
return formatRate(val, axis.tickDecimals);
}
function percentFormatter(val, axis) {
return formatPercent(val, axis.tickDecimals);
}
// Fetch data periodically and notify interested parties.
var listeners = [];
function subscribe(fn) {
listeners.push(fn);
}
function unsubscribe(fn) {
listeners = listeners.filter(function (el) {
if (el !== fn) {
return el;
}
});
}
var alertVisible = false;
function fetchData() {
function onDataReceived(stats) {
if (alertVisible) {
$(".alert-message").hide();
}
alertVisible = false;
for (var i = 0; i < listeners.length; i++) {
listeners[i](stats, stats.ekg.server_timestamp_ms.val);
}
}
function onError() {
$(".alert-message").show();
alertVisible = true;
}
$.ajax({
dataType: 'json',
success: onDataReceived,
error: onError,
cache: false
});
setTimeout(fetchData, updateInterval);
}
fetchData();
function addPlot(elem, series, opts) {
var defaultOptions = {
series: { shadowSize: 0 }, // drawing is faster without shadows
xaxis: { mode: "time", tickSize: [10, "second"] }
};
var options = $.extend(true, {}, defaultOptions, opts);
var data = new Array(series.length);
var maxPoints = 60;
for(var i = 0; i < series.length; i++) {
data[i] = [];
}
var plot = $.plot(elem, [], options);
var prev_stats, prev_time;
function onDataReceived(stats, time) {
for(var i = 0; i < series.length; i++) {
if (data[i].length >= maxPoints) {
data[i] = data[i].slice(1);
}
data[i].push([time, series[i].fn(stats, time,
prev_stats, prev_time)]);
}
// zip legends with data
var res = [];
for(var i = 0; i < series.length; i++)
res.push({ label: series[i].label, data: data[i] });
if (!paused) {
plot.setData(res);
plot.setupGrid();
plot.draw();
}
prev_stats = stats;
prev_time = time;
}
subscribe(onDataReceived);
return onDataReceived;
}
function addCounter(elem, fn, formatter) {
var prev_stats, prev_time;
function onDataReceived(stats, time) {
if (!paused)
elem.text(formatter(fn(stats, time, prev_stats, prev_time)));
prev_stats = stats;
prev_time = time;
}
subscribe(onDataReceived);
}
function addDynamicPlot(key, button, graph_fn, label_fn) {
function getStats(stats, time, prev_stats, prev_time) {
return graph_fn(key, stats, time, prev_stats, prev_time);
}
// jQuery has problem with IDs containing dots.
var plotId = key.replace(/\./g, "-") + "-plot";
$("#plots:last").append(
'<div id="' + plotId + '" class="plot-container">' +
'<img src="cross.png" class="close-button"><h3>' + key +
'</h3><div class="plot"></div></div>');
var plot = $("#plots > .plot-container:last > div");
var observer = addPlot(plot,
[{ label: label_fn(key), fn: getStats }],
{ yaxis: { tickFormatter: suffixFormatterGeneric } });
var plotContainer = $("#" + plotId);
var closeButton = plotContainer.find("img");
closeButton.hide();
closeButton.click(function () {
plotContainer.remove();
button.show();
unsubscribe(observer);
});
plotContainer.hover(
function () {
closeButton.show();
},
function () {
closeButton.hide();
}
);
}
function addMetrics(table) {
var COUNTER = "c";
var GAUGE = "g";
var metrics = {};
function makeDataGetter(key) {
var pieces = key.split(".");
function get(key, stats, time, prev_stats, prev_time) {
var value = stats;
$.each(pieces, function(unused_index, piece) {
value = value[piece];
});
if (value.type === COUNTER) {
if (prev_stats == undefined)
return null;
var prev_value = prev_stats;
$.each(pieces, function(unused_index, piece) {
prev_value = prev_value[piece];
});
return 1000 * (value.val - prev_value.val) /
(time - prev_time);
} else { // value.type === GAUGE
return value.val;
}
}
return get;
}
function counterLabel(label) {
return label + "/s";
}
function gaugeLabel(label) {
return label;
}
/** Adds the table row. */
function addElem(key, value) {
var elem;
if (key in metrics) {
elem = metrics[key];
} else {
// Add UI element
table.find("tbody:last").append(
'<tr><td>' + key +
' <img src="chart_line_add.png" class="graph-button"' +
' width="16" height="16"' +
' alt="Add graph" title="Add graph"></td>' +
'<td class="value">N/A</td></tr>');
elem = table.find("tbody > tr > td:last");
metrics[key] = elem;
var button = table.find("tbody > tr:last > td:first > img");
var graph_fn = makeDataGetter(key);
var label_fn = gaugeLabel;
if (value.type === COUNTER) {
label_fn = counterLabel;
}
button.click(function () {
addDynamicPlot(key, button, graph_fn, label_fn);
$(this).hide();
});
}
if (!paused) {
var val = value.val;
if ($.inArray(value.type, [COUNTER, GAUGE]) !== -1) {
val = commaify(val);
}
elem.text(val);
}
}
/** Updates UI for all metrics. */
function onDataReceived(stats, time) {
function build(prefix, obj) {
$.each(obj, function (suffix, value) {
if (value.hasOwnProperty("val")) {
var key = prefix + suffix;
addElem(key, value);
} else {
build(prefix + suffix + '.', value);
}
});
}
build('', stats);
}
subscribe(onDataReceived);
}
function initAll() {
// Metrics
var current_bytes_used = function (stats) {
return stats.rts.gc.current_bytes_used.val;
};
var max_bytes_used = function (stats) {
return stats.rts.gc.max_bytes_used.val;
};
var max_bytes_slop = function (stats) {
return stats.rts.gc.max_bytes_slop.val;
};
var current_bytes_slop = function (stats) {
return stats.rts.gc.current_bytes_slop.val;
};
var productivity_wall_percent = function (stats, time, prev_stats, prev_time) {
if (prev_stats == undefined)
return null;
var mutator_ms = stats.rts.gc.mutator_wall_ms.val -
prev_stats.rts.gc.mutator_wall_ms.val;
var gc_ms = stats.rts.gc.gc_wall_ms.val -
prev_stats.rts.gc.gc_wall_ms.val;
return 100 * mutator_ms / (mutator_ms + gc_ms);
};
var productivity_cpu_percent = function (stats, time, prev_stats, prev_time) {
if (prev_stats == undefined)
return null;
var mutator_ms = stats.rts.gc.mutator_cpu_ms.val -
prev_stats.rts.gc.mutator_cpu_ms.val;
var gc_ms = stats.rts.gc.gc_cpu_ms.val -
prev_stats.rts.gc.gc_cpu_ms.val;
return 100 * mutator_ms / (mutator_ms + gc_ms);
};
var allocation_rate = function (stats, time, prev_stats, prev_time) {
if (prev_stats == undefined)
return null;
return 1000 * (stats.rts.gc.bytes_allocated.val -
prev_stats.rts.gc.bytes_allocated.val) /
(time - prev_time);
};
addMetrics($("#metric-table"));
// Plots
addPlot($("#current-bytes-used-plot > div"),
[{ label: "residency", fn: current_bytes_used }],
{ yaxis: { tickFormatter: suffixFormatter } });
addPlot($("#allocation-rate-plot > div"),
[{ label: "rate", fn: allocation_rate }],
{ yaxis: { tickFormatter: rateFormatter } });
addPlot($("#productivity-plot > div"),
[{ label: "wall clock time", fn: productivity_wall_percent },
{ label: "cpu time", fn: productivity_cpu_percent }],
{ yaxis: { tickDecimals: 1, tickFormatter: percentFormatter } });
// GC and memory statistics
addCounter($("#max-bytes-used"), max_bytes_used, formatSuffix);
addCounter($("#current-bytes-used"), current_bytes_used, formatSuffix);
addCounter($("#max-bytes-slop"), max_bytes_slop, formatSuffix);
addCounter($("#current-bytes-slop"), current_bytes_slop, formatSuffix);
addCounter($("#productivity-wall"), productivity_wall_percent, formatPercent);
addCounter($("#productivity-cpu"), productivity_cpu_percent, formatPercent);
addCounter($("#allocation-rate"), allocation_rate, formatRate);
}
initAll();
});