|
|
|
|
@@ -16,19 +16,19 @@
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Object containing functions for displaying and updating the UI with streaming data.
|
|
|
|
|
*
|
|
|
|
|
*
|
|
|
|
|
* Publish this externally as "HystrixCommandMonitor"
|
|
|
|
|
*/
|
|
|
|
|
window.HystrixCommandMonitor = function(containerId, args) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var self = this; // keep scope under control
|
|
|
|
|
self.args = args;
|
|
|
|
|
if(self.args == undefined) {
|
|
|
|
|
self.args = {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.containerId = containerId;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Initialization on construction
|
|
|
|
|
*/
|
|
|
|
|
@@ -36,7 +36,7 @@
|
|
|
|
|
var maxXaxisForCircle="40%";
|
|
|
|
|
var maxYaxisForCircle="40%";
|
|
|
|
|
var maxRadiusForCircle="125";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// CIRCUIT_BREAKER circle visualization settings
|
|
|
|
|
self.circuitCircleRadius = d3.scale.pow().exponent(0.5).domain([0, 400]).range(["5", maxRadiusForCircle]); // requests per second per host
|
|
|
|
|
self.circuitCircleYaxis = d3.scale.linear().domain([0, 400]).range(["30%", maxXaxisForCircle]);
|
|
|
|
|
@@ -47,7 +47,7 @@
|
|
|
|
|
/**
|
|
|
|
|
* We want to keep sorting in the background since data values are always changing, so this will re-sort every X milliseconds
|
|
|
|
|
* to maintain whatever sort the user (or default) has chosen.
|
|
|
|
|
*
|
|
|
|
|
*
|
|
|
|
|
* In other words, sorting only for adds/deletes is not sufficient as all but alphabetical sort are dynamically changing.
|
|
|
|
|
*/
|
|
|
|
|
setInterval(function() {
|
|
|
|
|
@@ -55,11 +55,11 @@
|
|
|
|
|
self.sortSameAsLast();
|
|
|
|
|
}, 10000);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* END of Initialization on construction
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Event listener to handle new messages from EventSource as streamed from the server.
|
|
|
|
|
*/
|
|
|
|
|
@@ -70,7 +70,7 @@
|
|
|
|
|
if(!data.reportingHosts) {
|
|
|
|
|
data.reportingHosts = 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if(data && data.type == 'HystrixCommand') {
|
|
|
|
|
if (data.deleteData == 'true') {
|
|
|
|
|
deleteCircuit(data.escapedName);
|
|
|
|
|
@@ -82,34 +82,43 @@
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Pre process the data before displying in the UI.
|
|
|
|
|
* e.g Get Averages from sums, do rate calculation etc.
|
|
|
|
|
* Pre process the data before displying in the UI.
|
|
|
|
|
* e.g Get Averages from sums, do rate calculation etc.
|
|
|
|
|
*/
|
|
|
|
|
function preProcessData(data) {
|
|
|
|
|
// set defaults for values that may be missing from older streams
|
|
|
|
|
setIfMissing(data, "rollingCountBadRequests", 0);
|
|
|
|
|
// assert all the values we need
|
|
|
|
|
validateData(data);
|
|
|
|
|
// escape string used in jQuery & d3 selectors
|
|
|
|
|
data.escapedName = data.name.replace(/([ !"#$%&'()*+,./:;<=>?@[\]^`{|}~])/g,'\\$1');
|
|
|
|
|
// do math
|
|
|
|
|
converAllAvg(data);
|
|
|
|
|
convertAllAvg(data);
|
|
|
|
|
calcRatePerSecond(data);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function setIfMissing(data, key, defaultValue) {
|
|
|
|
|
if(data[key] == undefined) {
|
|
|
|
|
data[key] = defaultValue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Since the stream of data can be aggregated from multiple hosts in a tiered manner
|
|
|
|
|
* the aggregation just sums everything together and provides us the denominator (reportingHosts)
|
|
|
|
|
* so we must divide by it to get an average per instance value.
|
|
|
|
|
*
|
|
|
|
|
* so we must divide by it to get an average per instance value.
|
|
|
|
|
*
|
|
|
|
|
* We want to do this on any numerical values where we want per instance rather than cluster-wide sum.
|
|
|
|
|
*/
|
|
|
|
|
function converAllAvg(data) {
|
|
|
|
|
function convertAllAvg(data) {
|
|
|
|
|
convertAvg(data, "errorPercentage", true);
|
|
|
|
|
convertAvg(data, "latencyExecute_mean", false);
|
|
|
|
|
convertAvg(data, "latencyTotal_mean", false);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// the following will break when it becomes a compound string if the property is dynamically changed
|
|
|
|
|
convertAvg(data, "propertyValue_metricsRollingStatisticalWindowInMilliseconds", false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function convertAvg(data, key, decimal) {
|
|
|
|
|
if (decimal) {
|
|
|
|
|
data[key] = getInstanceAverage(data[key], data["reportingHosts"], decimal);
|
|
|
|
|
@@ -117,7 +126,7 @@
|
|
|
|
|
data[key] = getInstanceAverage(data[key], data["reportingHosts"], decimal);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function getInstanceAverage(value, reportingHosts, decimal) {
|
|
|
|
|
if (decimal) {
|
|
|
|
|
return roundNumber(value/reportingHosts);
|
|
|
|
|
@@ -125,7 +134,7 @@
|
|
|
|
|
return Math.floor(value/reportingHosts);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function calcRatePerSecond(data) {
|
|
|
|
|
var numberSeconds = data["propertyValue_metricsRollingStatisticalWindowInMilliseconds"] / 1000;
|
|
|
|
|
|
|
|
|
|
@@ -159,6 +168,7 @@
|
|
|
|
|
assertNotNull(data,"rollingCountSuccess");
|
|
|
|
|
assertNotNull(data,"rollingCountThreadPoolRejected");
|
|
|
|
|
assertNotNull(data,"rollingCountTimeout");
|
|
|
|
|
assertNotNull(data,"rollingCountBadRequests");
|
|
|
|
|
assertNotNull(data,"currentConcurrentExecutionCount");
|
|
|
|
|
assertNotNull(data,"latencyExecute_mean");
|
|
|
|
|
assertNotNull(data,"latencyExecute");
|
|
|
|
|
@@ -178,7 +188,7 @@
|
|
|
|
|
assertNotNull(data,"propertyValue_requestLogEnabled");
|
|
|
|
|
assertNotNull(data,"propertyValue_metricsRollingStatisticalWindowInMilliseconds");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function assertNotNull(data, key) {
|
|
|
|
|
if(data[key] == undefined) {
|
|
|
|
|
throw new Error("Key Missing: " + key + " for " + data.name);
|
|
|
|
|
@@ -187,25 +197,25 @@
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Method to display the CIRCUIT data
|
|
|
|
|
*
|
|
|
|
|
*
|
|
|
|
|
* @param data
|
|
|
|
|
*/
|
|
|
|
|
/* private */ function displayCircuit(data) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
preProcessData(data);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
log("Failed preProcessData: " + err.message);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// add the 'addCommas' function to the 'data' object so the HTML templates can use it
|
|
|
|
|
data.addCommas = addCommas;
|
|
|
|
|
// add the 'roundNumber' function to the 'data' object so the HTML templates can use it
|
|
|
|
|
data.roundNumber = roundNumber;
|
|
|
|
|
// add the 'getInstanceAverage' function to the 'data' object so the HTML templates can use it
|
|
|
|
|
data.getInstanceAverage = getInstanceAverage;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var addNew = false;
|
|
|
|
|
// check if we need to create the container
|
|
|
|
|
if(!$('#CIRCUIT_' + data.escapedName).length) {
|
|
|
|
|
@@ -215,39 +225,39 @@
|
|
|
|
|
}else {
|
|
|
|
|
data.includeDetailIcon = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// it doesn't exist so add it
|
|
|
|
|
var html = tmpl(hystrixTemplateCircuitContainer, data);
|
|
|
|
|
// remove the loading thing first
|
|
|
|
|
$('#' + containerId + ' span.loading').remove();
|
|
|
|
|
// now create the new data and add it
|
|
|
|
|
$('#' + containerId + '').append(html);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// add the default sparkline graph
|
|
|
|
|
d3.selectAll('#graph_CIRCUIT_' + data.escapedName + ' svg').append("svg:path");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// remember this is new so we can trigger a sort after setting data
|
|
|
|
|
addNew = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// now update/insert the data
|
|
|
|
|
$('#CIRCUIT_' + data.escapedName + ' div.monitor_data').html(tmpl(hystrixTemplateCircuit, data));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var ratePerSecond = data.ratePerSecond;
|
|
|
|
|
var ratePerSecondPerHost = data.ratePerSecondPerHost;
|
|
|
|
|
var ratePerSecondPerHostDisplay = ratePerSecondPerHost;
|
|
|
|
|
var errorThenVolume = (data.errorPercentage * 100000000) + ratePerSecond;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// set the rates on the div element so it's available for sorting
|
|
|
|
|
$('#CIRCUIT_' + data.escapedName).attr('rate_value', ratePerSecond);
|
|
|
|
|
$('#CIRCUIT_' + data.escapedName).attr('error_then_volume', errorThenVolume);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// update errorPercentage color on page
|
|
|
|
|
$('#CIRCUIT_' + data.escapedName + ' a.errorPercentage').css('color', self.circuitErrorPercentageColorRange(data.errorPercentage));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
updateCircle('circuit', '#CIRCUIT_' + data.escapedName + ' circle', ratePerSecondPerHostDisplay, data.errorPercentage);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if(data.graphValues) {
|
|
|
|
|
// we have a set of values to initialize with
|
|
|
|
|
updateSparkline('circuit', '#CIRCUIT_' + data.escapedName + ' path', data.graphValues);
|
|
|
|
|
@@ -260,7 +270,7 @@
|
|
|
|
|
self.sortSameAsLast();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* round a number to X digits: num => the number to round, dec => the number of decimals */
|
|
|
|
|
/* private */ function roundNumber(num) {
|
|
|
|
|
var dec=1;
|
|
|
|
|
@@ -271,10 +281,10 @@
|
|
|
|
|
}
|
|
|
|
|
return resultAsString;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* private */ function updateCircle(variablePrefix, cssTarget, rate, errorPercentage) {
|
|
|
|
|
var newXaxisForCircle = self[variablePrefix + 'CircleXaxis'](rate);
|
|
|
|
|
if(parseInt(newXaxisForCircle) > parseInt(maxXaxisForCircle)) {
|
|
|
|
|
@@ -288,7 +298,7 @@
|
|
|
|
|
if(parseInt(newRadiusForCircle) > parseInt(maxRadiusForCircle)) {
|
|
|
|
|
newRadiusForCircle = maxRadiusForCircle;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
d3.selectAll(cssTarget)
|
|
|
|
|
.transition()
|
|
|
|
|
.duration(400)
|
|
|
|
|
@@ -297,7 +307,7 @@
|
|
|
|
|
.attr("r", newRadiusForCircle)
|
|
|
|
|
.style("fill", self[variablePrefix + 'ColorRange'](errorPercentage));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* private */ function updateSparkline(variablePrefix, cssTarget, newDataPoint) {
|
|
|
|
|
var currentTimeMilliseconds = new Date().getTime();
|
|
|
|
|
var data = self[variablePrefix + cssTarget + '_data'];
|
|
|
|
|
@@ -313,40 +323,40 @@
|
|
|
|
|
self[variablePrefix + cssTarget + '_data'] = data;
|
|
|
|
|
} else {
|
|
|
|
|
if(typeof newDataPoint == 'object') {
|
|
|
|
|
/* if an array is passed in we'll replace the cached one */
|
|
|
|
|
/* if an array is passed in we'll replace the cached one */
|
|
|
|
|
data = newDataPoint;
|
|
|
|
|
} else {
|
|
|
|
|
// else we just add to the existing one
|
|
|
|
|
data.push({"v":parseFloat(newDataPoint),"t":currentTimeMilliseconds});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
while(data.length > 200) { // 400 should be plenty for the 2 minutes we have the scale set to below even with a very low update latency
|
|
|
|
|
// remove data so we don't keep increasing forever
|
|
|
|
|
// remove data so we don't keep increasing forever
|
|
|
|
|
data.shift();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(data.length == 1 && data[0].v == 0) {
|
|
|
|
|
//console.log("we have a single 0 so skipping");
|
|
|
|
|
// don't show if we have a single 0
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if(data.length > 1 && data[0].v == 0 && data[1].v != 0) {
|
|
|
|
|
//console.log("we have a leading 0 so removing it");
|
|
|
|
|
// get rid of a leading 0 if the following number is not a 0
|
|
|
|
|
data.shift();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var xScale = d3.time.scale().domain([new Date(currentTimeMilliseconds-(60*1000*2)), new Date(currentTimeMilliseconds)]).range([0, 140]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var yMin = d3.min(data, function(d) { return d.v; });
|
|
|
|
|
var yMax = d3.max(data, function(d) { return d.v; });
|
|
|
|
|
var yScale = d3.scale.linear().domain([yMin, yMax]).nice().range([60, 0]); // y goes DOWN, so 60 is the "lowest"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
sparkline = d3.svg.line()
|
|
|
|
|
// assign the X function to plot our line as we wish
|
|
|
|
|
.x(function(d,i) {
|
|
|
|
|
.x(function(d,i) {
|
|
|
|
|
// return the X coordinate where we want to plot this datapoint based on the time
|
|
|
|
|
return xScale(new Date(d.t));
|
|
|
|
|
})
|
|
|
|
|
@@ -354,14 +364,14 @@
|
|
|
|
|
return yScale(d.v);
|
|
|
|
|
})
|
|
|
|
|
.interpolate("basis");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
d3.selectAll(cssTarget).attr("d", sparkline(data));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* private */ function deleteCircuit(circuitName) {
|
|
|
|
|
$('#CIRCUIT_' + circuitName).remove();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// public methods for sorting
|
|
|
|
|
@@ -391,7 +401,7 @@
|
|
|
|
|
$('#' + this.containerId + ' div.monitor').tsort("p.name", {order: direction});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
HystrixCommandMonitor.prototype.sortByError = function() {
|
|
|
|
|
var direction = "desc";
|
|
|
|
|
if(this.sortedBy == 'error_desc') {
|
|
|
|
|
@@ -404,7 +414,7 @@
|
|
|
|
|
this.sortedBy = 'error_' + direction;
|
|
|
|
|
$('#' + this.containerId + ' div.monitor').tsort(".errorPercentage .value", {order: direction});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
HystrixCommandMonitor.prototype.sortByErrorThenVolume = function() {
|
|
|
|
|
var direction = "desc";
|
|
|
|
|
if(this.sortedBy == 'error_then_volume_desc') {
|
|
|
|
|
@@ -412,7 +422,7 @@
|
|
|
|
|
}
|
|
|
|
|
this.sortByErrorThenVolumeInDirection(direction);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
HystrixCommandMonitor.prototype.sortByErrorThenVolumeInDirection = function(direction) {
|
|
|
|
|
this.sortedBy = 'error_then_volume_' + direction;
|
|
|
|
|
$('#' + this.containerId + ' div.monitor').tsort({order: direction, attr: 'error_then_volume'});
|
|
|
|
|
@@ -484,7 +494,7 @@
|
|
|
|
|
} else if(this.sortedBy == 'error_then_volume_asc') {
|
|
|
|
|
this.sortByErrorThenVolumeInDirection('asc');
|
|
|
|
|
} else if(this.sortedBy == 'error_then_volume_desc') {
|
|
|
|
|
this.sortByErrorThenVolumeInDirection('desc');
|
|
|
|
|
this.sortByErrorThenVolumeInDirection('desc');
|
|
|
|
|
} else if(this.sortedBy == 'lat90_asc') {
|
|
|
|
|
this.sortByMetricInDirection('asc', '.latency90 .value');
|
|
|
|
|
} else if(this.sortedBy == 'lat90_desc') {
|
|
|
|
|
@@ -505,7 +515,7 @@
|
|
|
|
|
this.sortByMetricInDirection('asc', '.latencyMedian .value');
|
|
|
|
|
} else if(this.sortedBy == 'latMedian_desc') {
|
|
|
|
|
this.sortByMetricInDirection('desc', '.latencyMedian .value');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// default sort type and direction
|
|
|
|
|
|