diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/hystrix/annotations/EnableHystrixDashboard.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/hystrix/annotations/EnableHystrixDashboard.java index 3537346b..abf27bd7 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/hystrix/annotations/EnableHystrixDashboard.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/hystrix/annotations/EnableHystrixDashboard.java @@ -6,7 +6,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import org.springframework.cloud.netflix.hystrix.HystrixDashboardConfiguration; +import org.springframework.cloud.netflix.hystrix.dashboard.HystrixDashboardConfiguration; import org.springframework.context.annotation.Import; /** diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/hystrix/HystrixDashboardConfiguration.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/hystrix/dashboard/HystrixDashboardConfiguration.java similarity index 99% rename from spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/hystrix/HystrixDashboardConfiguration.java rename to spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/hystrix/dashboard/HystrixDashboardConfiguration.java index 3ff80319..a584dc14 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/hystrix/HystrixDashboardConfiguration.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/hystrix/dashboard/HystrixDashboardConfiguration.java @@ -1,4 +1,4 @@ -package org.springframework.cloud.netflix.hystrix; +package org.springframework.cloud.netflix.hystrix.dashboard; import java.io.IOException; import java.io.InputStream; diff --git a/spring-cloud-netflix-core/src/main/resources/static/hystrix/components/hystrixCommand/hystrixCommand.css b/spring-cloud-netflix-core/src/main/resources/static/hystrix/components/hystrixCommand/hystrixCommand.css new file mode 100644 index 00000000..71d5ee39 --- /dev/null +++ b/spring-cloud-netflix-core/src/main/resources/static/hystrix/components/hystrixCommand/hystrixCommand.css @@ -0,0 +1,196 @@ +.dependencies .spacer { + width: 100%; + margin: 0 auto; + padding-top:4px; + clear:both; +} + + +.dependencies .last { + margin-right: 0px; +} + +.dependencies span.loading { + display: block; + padding-top: 6%; + padding-bottom: 6%; + color: gray; + text-align: center; +} + +.dependencies span.loading.failed { + color: red; +} + + +.dependencies div.monitor { + float: left; + margin-right:5px; + margin-top:5px; +} + +.dependencies div.monitor p.name { + font-weight:bold; + font-size: 10pt; + text-align: right; + padding-bottom: 5px; +} + +.dependencies div.monitor_data { + margin: 0 auto; +} + +/* override the HREF when we have specified it as a tooltip to not act like a link */ +.dependencies div.monitor_data a.tooltip { + text-decoration: none; + cursor: default; +} + +.dependencies div.monitor_data div.counters { + text-align: right; + padding-bottom: 10px; + font-size: 10pt; + clear: both; + +} + +.dependencies div.monitor_data div.counters div.cell { + display: inline; + float: right; +} + +.dependencies .borderRight { + border-right: 1px solid grey; + padding-right: 6px; + padding-left: 8px; +} + +.dependencies div.cell .line { + display: block; +} + +.dependencies div.monitor_data a, +.dependencies span.rate_value { + font-weight:bold; +} + + +.dependencies span.smaller { + font-size: 8pt; + color: grey; +} + + + +.dependencies div.tableRow { + width:100%; + white-space: nowrap; + font-size: 8pt; + margin: 0 auto; + clear:both; + padding-left:26%; +} + +.dependencies div.tableRow .cell { + float:left; +} + +.dependencies div.tableRow .header { + width:18%; + text-align:right; + padding-right:2%; +} + +.dependencies div.tableRow .data { + width:17%; + font-weight: bold; + text-align:right; +} + + +.dependencies div.monitor { + width: 245px; /* we want a fixed width instead of percentage as I want the boxes to be a set size and then fill in as many as can fit in each row ... this allows 3 columns on an iPad */ + height: 150px; +} + +.dependencies .success { + color: green; +} +.dependencies .shortCircuited { + color: blue; +} +.dependencies .timeout { + color: #FF9900; /* shade of orange */ +} +.dependencies .failure { + color: red; +} + +.dependencies .rejected { + color: purple; +} + +.dependencies .exceptionsThrown { + color: brown; +} + +.dependencies div.monitor_data a.rate { + color: black; + font-size: 11pt; +} + +.dependencies div.rate { + padding-top: 1px; + clear:both; + text-align:right; +} + +.dependencies .errorPercentage { + color: grey; +} + +.dependencies div.cell .errorPercentage { + padding-left:5px; + font-size: 12pt !important; +} + + +.dependencies div.monitor div.chart { +} + +.dependencies div.monitor div.chart svg { +} + +.dependencies div.monitor div.chart svg text { + fill: white; +} + + +.dependencies div.circuitStatus { + width:100%; + white-space: nowrap; + font-size: 9pt; + margin: 0 auto; + clear:both; + text-align:right; + padding-top: 4px; +} + +.dependencies #hidden { + width:1px; + height:1px; + background: lightgrey; + display: none; +} + + + +/* sparkline */ +.dependencies path { + stroke: steelblue; + stroke-width: 1; + fill: none; +} + + +} diff --git a/spring-cloud-netflix-core/src/main/resources/static/hystrix/components/hystrixCommand/hystrixCommand.js b/spring-cloud-netflix-core/src/main/resources/static/hystrix/components/hystrixCommand/hystrixCommand.js new file mode 100644 index 00000000..dc375759 --- /dev/null +++ b/spring-cloud-netflix-core/src/main/resources/static/hystrix/components/hystrixCommand/hystrixCommand.js @@ -0,0 +1,536 @@ + +(function(window) { + + // cache the templates we use on this page as global variables (asynchronously) + jQuery.get(getRelativePath("../components/hystrixCommand/templates/hystrixCircuit.html"), function(data) { + hystrixTemplateCircuit = data; + }); + jQuery.get(getRelativePath("../components/hystrixCommand/templates/hystrixCircuitContainer.html"), function(data) { + hystrixTemplateCircuitContainer = data; + }); + + function getRelativePath(path) { + var p = location.pathname.slice(0, location.pathname.lastIndexOf("/")+1); + return p + path; + } + + /** + * 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 + */ + // intialize various variables we use for visualization + 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]); + self.circuitCircleXaxis = d3.scale.linear().domain([0, 400]).range(["30%", maxYaxisForCircle]); + self.circuitColorRange = d3.scale.linear().domain([10, 25, 40, 50]).range(["green", "#FFCC00", "#FF9900", "red"]); + self.circuitErrorPercentageColorRange = d3.scale.linear().domain([0, 10, 35, 50]).range(["grey", "black", "#FF9900", "red"]); + + /** + * 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() { + // sort since we have added a new one + self.sortSameAsLast(); + }, 10000); + + + /** + * END of Initialization on construction + */ + + /** + * Event listener to handle new messages from EventSource as streamed from the server. + */ + /* public */ self.eventSourceMessageListener = function(e) { + var data = JSON.parse(e.data); + if(data) { + // check for reportingHosts (if not there, set it to 1 for singleHost vs cluster) + if(!data.reportingHosts) { + data.reportingHosts = 1; + } + + if(data && data.type == 'HystrixCommand') { + if (data.deleteData == 'true') { + deleteCircuit(data.escapedName); + } else { + displayCircuit(data); + } + } + } + }; + + /** + * Pre process the data before displying in the UI. + * e.g Get Averages from sums, do rate calculation etc. + */ + function preProcessData(data) { + validateData(data); + // escape string used in jQuery & d3 selectors + data.escapedName = data.name.replace(/([ !"#$%&'()*+,./:;<=>?@[\]^`{|}~])/g,'\\$1'); + // do math + converAllAvg(data); + calcRatePerSecond(data); + } + + /** + * 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. + * + * We want to do this on any numerical values where we want per instance rather than cluster-wide sum. + */ + function converAllAvg(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); + } else { + data[key] = getInstanceAverage(data[key], data["reportingHosts"], decimal); + } + } + + function getInstanceAverage(value, reportingHosts, decimal) { + if (decimal) { + return roundNumber(value/reportingHosts); + } else { + return Math.floor(value/reportingHosts); + } + } + + function calcRatePerSecond(data) { + var numberSeconds = data["propertyValue_metricsRollingStatisticalWindowInMilliseconds"] / 1000; + + var totalRequests = data["requestCount"]; + if (totalRequests < 0) { + totalRequests = 0; + } + data["ratePerSecond"] = roundNumber(totalRequests / numberSeconds); + data["ratePerSecondPerHost"] = roundNumber(totalRequests / numberSeconds / data["reportingHosts"]) ; + } + + function validateData(data) { + assertNotNull(data,"reportingHosts"); + assertNotNull(data,"type"); + assertNotNull(data,"name"); + assertNotNull(data,"group"); + // assertNotNull(data,"currentTime"); + assertNotNull(data,"isCircuitBreakerOpen"); + assertNotNull(data,"errorPercentage"); + assertNotNull(data,"errorCount"); + assertNotNull(data,"requestCount"); + assertNotNull(data,"rollingCountCollapsedRequests"); + assertNotNull(data,"rollingCountExceptionsThrown"); + assertNotNull(data,"rollingCountFailure"); + assertNotNull(data,"rollingCountFallbackFailure"); + assertNotNull(data,"rollingCountFallbackRejection"); + assertNotNull(data,"rollingCountFallbackSuccess"); + assertNotNull(data,"rollingCountResponsesFromCache"); + assertNotNull(data,"rollingCountSemaphoreRejected"); + assertNotNull(data,"rollingCountShortCircuited"); + assertNotNull(data,"rollingCountSuccess"); + assertNotNull(data,"rollingCountThreadPoolRejected"); + assertNotNull(data,"rollingCountTimeout"); + assertNotNull(data,"currentConcurrentExecutionCount"); + assertNotNull(data,"latencyExecute_mean"); + assertNotNull(data,"latencyExecute"); + assertNotNull(data,"latencyTotal_mean"); + assertNotNull(data,"latencyTotal"); + assertNotNull(data,"propertyValue_circuitBreakerRequestVolumeThreshold"); + assertNotNull(data,"propertyValue_circuitBreakerSleepWindowInMilliseconds"); + assertNotNull(data,"propertyValue_circuitBreakerErrorThresholdPercentage"); + assertNotNull(data,"propertyValue_circuitBreakerForceOpen"); + assertNotNull(data,"propertyValue_executionIsolationStrategy"); + assertNotNull(data,"propertyValue_executionIsolationThreadTimeoutInMilliseconds"); + assertNotNull(data,"propertyValue_executionIsolationThreadInterruptOnTimeout"); + // assertNotNull(data,"propertyValue_executionIsolationThreadPoolKeyOverride"); + assertNotNull(data,"propertyValue_executionIsolationSemaphoreMaxConcurrentRequests"); + assertNotNull(data,"propertyValue_fallbackIsolationSemaphoreMaxConcurrentRequests"); + assertNotNull(data,"propertyValue_requestCacheEnabled"); + 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); + } + } + + /** + * 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) { + // args for display + if(self.args.includeDetailIcon != undefined && self.args.includeDetailIcon) { + data.includeDetailIcon = true; + }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); + } else { + updateSparkline('circuit', '#CIRCUIT_' + data.escapedName + ' path', ratePerSecond); + } + + if(addNew) { + // sort since we added a new circuit + 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; + var result = Math.round(num*Math.pow(10,dec))/Math.pow(10,dec); + var resultAsString = result.toString(); + if(resultAsString.indexOf('.') == -1) { + resultAsString = resultAsString + '.0'; + } + return resultAsString; + }; + + + + + /* private */ function updateCircle(variablePrefix, cssTarget, rate, errorPercentage) { + var newXaxisForCircle = self[variablePrefix + 'CircleXaxis'](rate); + if(parseInt(newXaxisForCircle) > parseInt(maxXaxisForCircle)) { + newXaxisForCircle = maxXaxisForCircle; + } + var newYaxisForCircle = self[variablePrefix + 'CircleYaxis'](rate); + if(parseInt(newYaxisForCircle) > parseInt(maxYaxisForCircle)) { + newYaxisForCircle = maxYaxisForCircle; + } + var newRadiusForCircle = self[variablePrefix + 'CircleRadius'](rate); + if(parseInt(newRadiusForCircle) > parseInt(maxRadiusForCircle)) { + newRadiusForCircle = maxRadiusForCircle; + } + + d3.selectAll(cssTarget) + .transition() + .duration(400) + .attr("cy", newYaxisForCircle) + .attr("cx", newXaxisForCircle) + .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']; + if(typeof data == 'undefined') { + // else it's new + if(typeof newDataPoint == 'object') { + // we received an array of values, so initialize with it + data = newDataPoint; + } else { + // v: VALUE, t: TIME_IN_MILLISECONDS + data = [{"v":parseFloat(newDataPoint),"t":currentTimeMilliseconds}]; + } + self[variablePrefix + cssTarget + '_data'] = data; + } else { + if(typeof newDataPoint == 'object') { + /* 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 + 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) { + // return the X coordinate where we want to plot this datapoint based on the time + return xScale(new Date(d.t)); + }) + .y(function(d) { + return yScale(d.v); + }) + .interpolate("basis"); + + d3.selectAll(cssTarget).attr("d", sparkline(data)); + } + + /* private */ function deleteCircuit(circuitName) { + $('#CIRCUIT_' + circuitName).remove(); + } + + }; + + // public methods for sorting + HystrixCommandMonitor.prototype.sortByVolume = function() { + var direction = "desc"; + if(this.sortedBy == 'rate_desc') { + direction = 'asc'; + } + this.sortByVolumeInDirection(direction); + }; + + HystrixCommandMonitor.prototype.sortByVolumeInDirection = function(direction) { + this.sortedBy = 'rate_' + direction; + $('#' + this.containerId + ' div.monitor').tsort({order: direction, attr: 'rate_value'}); + }; + + HystrixCommandMonitor.prototype.sortAlphabetically = function() { + var direction = "asc"; + if(this.sortedBy == 'alph_asc') { + direction = 'desc'; + } + this.sortAlphabeticalInDirection(direction); + }; + + HystrixCommandMonitor.prototype.sortAlphabeticalInDirection = function(direction) { + this.sortedBy = 'alph_' + direction; + $('#' + this.containerId + ' div.monitor').tsort("p.name", {order: direction}); + }; + + + HystrixCommandMonitor.prototype.sortByError = function() { + var direction = "desc"; + if(this.sortedBy == 'error_desc') { + direction = 'asc'; + } + this.sortByErrorInDirection(direction); + }; + + HystrixCommandMonitor.prototype.sortByErrorInDirection = function(direction) { + 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') { + direction = 'asc'; + } + 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'}); + }; + + HystrixCommandMonitor.prototype.sortByLatency90 = function() { + var direction = "desc"; + if(this.sortedBy == 'lat90_desc') { + direction = 'asc'; + } + this.sortedBy = 'lat90_' + direction; + this.sortByMetricInDirection(direction, ".latency90 .value"); + }; + + HystrixCommandMonitor.prototype.sortByLatency99 = function() { + var direction = "desc"; + if(this.sortedBy == 'lat99_desc') { + direction = 'asc'; + } + this.sortedBy = 'lat99_' + direction; + this.sortByMetricInDirection(direction, ".latency99 .value"); + }; + + HystrixCommandMonitor.prototype.sortByLatency995 = function() { + var direction = "desc"; + if(this.sortedBy == 'lat995_desc') { + direction = 'asc'; + } + this.sortedBy = 'lat995_' + direction; + this.sortByMetricInDirection(direction, ".latency995 .value"); + }; + + HystrixCommandMonitor.prototype.sortByLatencyMean = function() { + var direction = "desc"; + if(this.sortedBy == 'latMean_desc') { + direction = 'asc'; + } + this.sortedBy = 'latMean_' + direction; + this.sortByMetricInDirection(direction, ".latencyMean .value"); + }; + + HystrixCommandMonitor.prototype.sortByLatencyMedian = function() { + var direction = "desc"; + if(this.sortedBy == 'latMedian_desc') { + direction = 'asc'; + } + this.sortedBy = 'latMedian_' + direction; + this.sortByMetricInDirection(direction, ".latencyMedian .value"); + }; + + HystrixCommandMonitor.prototype.sortByMetricInDirection = function(direction, metric) { + $('#' + this.containerId + ' div.monitor').tsort(metric, {order: direction}); + }; + + // this method is for when new divs are added to cause the elements to be sorted to whatever the user last chose + HystrixCommandMonitor.prototype.sortSameAsLast = function() { + if(this.sortedBy == 'alph_asc') { + this.sortAlphabeticalInDirection('asc'); + } else if(this.sortedBy == 'alph_desc') { + this.sortAlphabeticalInDirection('desc'); + } else if(this.sortedBy == 'rate_asc') { + this.sortByVolumeInDirection('asc'); + } else if(this.sortedBy == 'rate_desc') { + this.sortByVolumeInDirection('desc'); + } else if(this.sortedBy == 'error_asc') { + this.sortByErrorInDirection('asc'); + } else if(this.sortedBy == 'error_desc') { + this.sortByErrorInDirection('desc'); + } else if(this.sortedBy == 'error_then_volume_asc') { + this.sortByErrorThenVolumeInDirection('asc'); + } else if(this.sortedBy == 'error_then_volume_desc') { + this.sortByErrorThenVolumeInDirection('desc'); + } else if(this.sortedBy == 'lat90_asc') { + this.sortByMetricInDirection('asc', '.latency90 .value'); + } else if(this.sortedBy == 'lat90_desc') { + this.sortByMetricInDirection('desc', '.latency90 .value'); + } else if(this.sortedBy == 'lat99_asc') { + this.sortByMetricInDirection('asc', '.latency99 .value'); + } else if(this.sortedBy == 'lat99_desc') { + this.sortByMetricInDirection('desc', '.latency99 .value'); + } else if(this.sortedBy == 'lat995_asc') { + this.sortByMetricInDirection('asc', '.latency995 .value'); + } else if(this.sortedBy == 'lat995_desc') { + this.sortByMetricInDirection('desc', '.latency995 .value'); + } else if(this.sortedBy == 'latMean_asc') { + this.sortByMetricInDirection('asc', '.latencyMean .value'); + } else if(this.sortedBy == 'latMean_desc') { + this.sortByMetricInDirection('desc', '.latencyMean .value'); + } else if(this.sortedBy == 'latMedian_asc') { + this.sortByMetricInDirection('asc', '.latencyMedian .value'); + } else if(this.sortedBy == 'latMedian_desc') { + this.sortByMetricInDirection('desc', '.latencyMedian .value'); + } + }; + + // default sort type and direction + this.sortedBy = 'alph_asc'; + + + // a temporary home for the logger until we become more sophisticated + function log(message) { + console.log(message); + }; + + function addCommas(nStr){ + nStr += ''; + if(nStr.length <=3) { + return nStr; //shortcut if we don't need commas + } + x = nStr.split('.'); + x1 = x[0]; + x2 = x.length > 1 ? '.' + x[1] : ''; + var rgx = /(\d+)(\d{3})/; + while (rgx.test(x1)) { + x1 = x1.replace(rgx, '$1' + ',' + '$2'); + } + return x1 + x2; + } +})(window); + + diff --git a/spring-cloud-netflix-core/src/main/resources/static/hystrix/components/hystrixCommand/magnifying-glass-icon-20.png b/spring-cloud-netflix-core/src/main/resources/static/hystrix/components/hystrixCommand/magnifying-glass-icon-20.png new file mode 100644 index 00000000..2833fe2c Binary files /dev/null and b/spring-cloud-netflix-core/src/main/resources/static/hystrix/components/hystrixCommand/magnifying-glass-icon-20.png differ diff --git a/spring-cloud-netflix-core/src/main/resources/static/hystrix/components/hystrixCommand/magnifying-glass-icon.png b/spring-cloud-netflix-core/src/main/resources/static/hystrix/components/hystrixCommand/magnifying-glass-icon.png new file mode 100644 index 00000000..e007dd39 Binary files /dev/null and b/spring-cloud-netflix-core/src/main/resources/static/hystrix/components/hystrixCommand/magnifying-glass-icon.png differ diff --git a/spring-cloud-netflix-core/src/main/resources/static/hystrix/components/hystrixCommand/templates/hystrixCircuit.html b/spring-cloud-netflix-core/src/main/resources/static/hystrix/components/hystrixCommand/templates/hystrixCircuit.html new file mode 100644 index 00000000..e50a3524 --- /dev/null +++ b/spring-cloud-netflix-core/src/main/resources/static/hystrix/components/hystrixCommand/templates/hystrixCircuit.html @@ -0,0 +1,73 @@ +
+ +
+ <%= errorPercentage %> % +
+ +
+ <% if(propertyValue_executionIsolationStrategy == 'THREAD') { %> + <%= addCommas(rollingCountTimeout) %> + <%= addCommas(rollingCountThreadPoolRejected) %> + <% } %> + <%= addCommas(rollingCountFailure) %> +
+
+ <%= addCommas(rollingCountSuccess) %> + <%= addCommas(rollingCountShortCircuited) %> +
+
+
+ +
+ Host: <%= addCommas(roundNumber(ratePerSecondPerHost)) %>/s +
+
+ Cluster: <%= addCommas(roundNumber(ratePerSecond)) %>/s +
+ +
+ <% if(propertyValue_circuitBreakerForceClosed) { %> + [ Forced Closed ] + <% } %> + <% if(propertyValue_circuitBreakerForceOpen) { %> + Circuit Forced Open + <% } else { %> + <% if(isCircuitBreakerOpen == reportingHosts) { %> + Circuit Open + <% } else if(isCircuitBreakerOpen == 0) { %> + Circuit Closed + <% } else { + /* We have some circuits that are open */ + %> + Circuit <%= isCircuitBreakerOpen.replace("true", "Open").replace("false", "Closed") %>) + <% } %> + <% } %> +
+ +
+ +
+ <% if(typeof reportingHosts != 'undefined') { %> +
Hosts
+
<%= reportingHosts %>
+ <% } else { %> +
Host
+
Single
+ <% } %> +
90th
+
<%= getInstanceAverage(latencyExecute['90'], reportingHosts, false) %>ms
+
+
+
Median
+
<%= getInstanceAverage(latencyExecute['50'], reportingHosts, false) %>ms
+
99th
+
<%= getInstanceAverage(latencyExecute['99'], reportingHosts, false) %>ms
+
+
+
Mean
+
<%= latencyExecute_mean %>ms
+
99.5th
+
<%= getInstanceAverage(latencyExecute['99.5'], reportingHosts, false) %>ms
+
+ + diff --git a/spring-cloud-netflix-core/src/main/resources/static/hystrix/components/hystrixCommand/templates/hystrixCircuitContainer.html b/spring-cloud-netflix-core/src/main/resources/static/hystrix/components/hystrixCommand/templates/hystrixCircuitContainer.html new file mode 100644 index 00000000..4d19dbe0 --- /dev/null +++ b/spring-cloud-netflix-core/src/main/resources/static/hystrix/components/hystrixCommand/templates/hystrixCircuitContainer.html @@ -0,0 +1,40 @@ +
+ <% + var displayName = name; + var toolTip = ""; + if(displayName.length > 32) { + displayName = displayName.substring(0,4) + "..." + displayName.substring(displayName.length-20, displayName.length); + toolTip = "title=\"" + name + "\""; + } + %> + +
+
+ <% if(includeDetailIcon) { %> +

style="padding-right:16px"> + <%= displayName %> + +

+ <% } else { %> +

><%= displayName %>

+ <% } %> +
+
+
+
+
+ + +
diff --git a/spring-cloud-netflix-core/src/main/resources/static/hystrix/components/hystrixCommand/templates/hystrixCircuitProperties.html b/spring-cloud-netflix-core/src/main/resources/static/hystrix/components/hystrixCommand/templates/hystrixCircuitProperties.html new file mode 100644 index 00000000..5b8c0fae --- /dev/null +++ b/spring-cloud-netflix-core/src/main/resources/static/hystrix/components/hystrixCommand/templates/hystrixCircuitProperties.html @@ -0,0 +1,6 @@ +
+
Median
+
<%= sla_medianLastMinute %>ms
+
99th
+
<%= sla_percentile99LastMinute %>ms
+
diff --git a/spring-cloud-netflix-core/src/main/resources/static/hystrix/components/hystrixThreadPool/hystrixThreadPool.css b/spring-cloud-netflix-core/src/main/resources/static/hystrix/components/hystrixThreadPool/hystrixThreadPool.css new file mode 100644 index 00000000..e82ea5cc --- /dev/null +++ b/spring-cloud-netflix-core/src/main/resources/static/hystrix/components/hystrixThreadPool/hystrixThreadPool.css @@ -0,0 +1,141 @@ +.dependencyThreadPools .spacer { + width: 100%; + margin: 0 auto; + padding-top:4px; + clear:both; +} + + +.dependencyThreadPools .last { + margin-right: 0px; +} + +.dependencyThreadPools span.loading { + display: block; + padding-top: 6%; + padding-bottom: 6%; + color: gray; + text-align: center; +} + +.dependencyThreadPools span.loading.failed { + color: red; +} + + +.dependencyThreadPools div.monitor { + float: left; + margin-right:5px; /* these are tweaked to look good on desktop and iPad portrait, and fit things densely */ + margin-top:5px; +} + +.dependencyThreadPools div.monitor p.name { + font-weight:bold; + font-size: 10pt; + text-align: right; + padding-bottom: 5px; +} + +.dependencyThreadPools div.monitor_data { + margin: 0 auto; +} + +.dependencyThreadPools span.smaller { + font-size: 8pt; + color: grey; +} + + +.dependencyThreadPools div.tableRow { + width:100%; + white-space: nowrap; + font-size: 8pt; + margin: 0 auto; + clear:both; +} + +.dependencyThreadPools div.tableRow .cell { + float:left; +} + +.dependencyThreadPools div.tableRow .header { + text-align:right; + padding-right:5px; +} + +.dependencyThreadPools div.tableRow .header.left { + width:85px; +} + +.dependencyThreadPools div.tableRow .header.right { + width:75px; +} + +.dependencyThreadPools div.tableRow .data { + font-weight: bold; + text-align:right; +} + +.dependencyThreadPools div.tableRow .data.left { + width:30px; +} + +.dependencyThreadPools div.tableRow .data.right { + width:45px; +} + +.dependencyThreadPools div.monitor { + width: 245px; /* we want a fixed width instead of percentage as I want the boxes to be a set size and then fill in as many as can fit in each row ... this allows 3 columns on an iPad */ + height: 110px; +} + + + + + +/* override the HREF when we have specified it as a tooltip to not act like a link */ +.dependencyThreadPools div.monitor_data a.tooltip { + text-decoration: none; + cursor: default; +} + +.dependencyThreadPools div.monitor_data a.rate { + font-weight:bold; + color: black; + font-size: 11pt; +} + +.dependencyThreadPools div.rate { + padding-top: 1px; + clear:both; + text-align:right; +} + +.dependencyThreadPools span.rate_value { + font-weight:bold; +} + + + + + + + +.dependencyThreadPools div.monitor div.chart { +} + +.dependencyThreadPools div.monitor div.chart svg { +} + +.dependencyThreadPools div.monitor div.chart svg text { + fill: white; +} + +.dependencyThreadPools #hidden { + width:1px; + height:1px; + background: lightgrey; + display: none; +} + + diff --git a/spring-cloud-netflix-core/src/main/resources/static/hystrix/components/hystrixThreadPool/hystrixThreadPool.js b/spring-cloud-netflix-core/src/main/resources/static/hystrix/components/hystrixThreadPool/hystrixThreadPool.js new file mode 100644 index 00000000..a5003bc4 --- /dev/null +++ b/spring-cloud-netflix-core/src/main/resources/static/hystrix/components/hystrixThreadPool/hystrixThreadPool.js @@ -0,0 +1,346 @@ + +(function(window) { + + // cache the templates we use on this page as global variables (asynchronously) + jQuery.get(getRelativePath("../components/hystrixThreadPool/templates/hystrixThreadPool.html"), function(data) { + htmlTemplate = data; + }); + jQuery.get(getRelativePath("../components/hystrixThreadPool/templates/hystrixThreadPoolContainer.html"), function(data) { + htmlTemplateContainer = data; + }); + + function getRelativePath(path) { + var p = location.pathname.slice(0, location.pathname.lastIndexOf("/")+1); + return p + path; + } + + /** + * Object containing functions for displaying and updating the UI with streaming data. + * + * Publish this externally as "HystrixThreadPoolMonitor" + */ + window.HystrixThreadPoolMonitor = function(containerId) { + + var self = this; // keep scope under control + + this.containerId = containerId; + + /** + * Initialization on construction + */ + // intialize various variables we use for visualization + var maxXaxisForCircle="40%"; + var maxYaxisForCircle="40%"; + var maxRadiusForCircle="125"; + var maxDomain = 2000; + + self.circleRadius = d3.scale.pow().exponent(0.5).domain([0, maxDomain]).range(["5", maxRadiusForCircle]); // requests per second per host + self.circleYaxis = d3.scale.linear().domain([0, maxDomain]).range(["30%", maxXaxisForCircle]); + self.circleXaxis = d3.scale.linear().domain([0, maxDomain]).range(["30%", maxYaxisForCircle]); + self.colorRange = d3.scale.linear().domain([10, 25, 40, 50]).range(["green", "#FFCC00", "#FF9900", "red"]); + self.errorPercentageColorRange = d3.scale.linear().domain([0, 10, 35, 50]).range(["grey", "black", "#FF9900", "red"]); + + /** + * 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() { + // sort since we have added a new one + self.sortSameAsLast(); + }, 1000) + + /** + * END of Initialization on construction + */ + + /** + * Event listener to handle new messages from EventSource as streamed from the server. + */ + /* public */ self.eventSourceMessageListener = function(e) { + var data = JSON.parse(e.data); + if(data) { + // check for reportingHosts (if not there, set it to 1 for singleHost vs cluster) + if(!data.reportingHosts) { + data.reportingHosts = 1; + } + + if(data && data.type == 'HystrixThreadPool') { + if (data.deleteData == 'true') { + deleteThreadPool(data.escapedName); + } else { + displayThreadPool(data); + } + } + } + } + + /** + * Pre process the data before displying in the UI. + * e.g Get Averages from sums, do rate calculation etc. + */ + function preProcessData(data) { + validateData(data); + // escape string used in jQuery & d3 selectors + data.escapedName = data.name.replace(/([ !"#$%&'()*+,./:;<=>?@[\]^`{|}~])/g,'\\$1'); + // do math + converAllAvg(data); + calcRatePerSecond(data); + } + + function converAllAvg(data) { + convertAvg(data, "propertyValue_queueSizeRejectionThreshold", 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] = roundNumber(data[key]/data["reportingHosts"]); + } else { + data[key] = Math.floor(data[key]/data["reportingHosts"]); + } + } + + function calcRatePerSecond(data) { + var numberSeconds = data["propertyValue_metricsRollingStatisticalWindowInMilliseconds"] / 1000; + + var totalThreadsExecuted = data["rollingCountThreadsExecuted"]; + if (totalThreadsExecuted < 0) { + totalThreadsExecuted = 0; + } + data["ratePerSecond"] = roundNumber(totalThreadsExecuted / numberSeconds); + data["ratePerSecondPerHost"] = roundNumber(totalThreadsExecuted / numberSeconds / data["reportingHosts"]); + } + + function validateData(data) { + + assertNotNull(data,"type"); + assertNotNull(data,"name"); + // assertNotNull(data,"currentTime"); + assertNotNull(data,"currentActiveCount"); + assertNotNull(data,"currentCompletedTaskCount"); + assertNotNull(data,"currentCorePoolSize"); + assertNotNull(data,"currentLargestPoolSize"); + assertNotNull(data,"currentMaximumPoolSize"); + assertNotNull(data,"currentPoolSize"); + assertNotNull(data,"currentQueueSize"); + assertNotNull(data,"currentTaskCount"); + assertNotNull(data,"rollingCountThreadsExecuted"); + assertNotNull(data,"rollingMaxActiveThreads"); + assertNotNull(data,"reportingHosts"); + + assertNotNull(data,"propertyValue_queueSizeRejectionThreshold"); + assertNotNull(data,"propertyValue_metricsRollingStatisticalWindowInMilliseconds"); + } + + function assertNotNull(data, key) { + if(data[key] == undefined) { + if (key == "dependencyOwner") { + data["dependencyOwner"] = data.name; + } else { + throw new Error("Key Missing: " + key + " for " + data.name) + } + } + } + + /** + * Method to display the THREAD_POOL data + * + * @param data + */ + /* private */ function displayThreadPool(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; + + var addNew = false; + // check if we need to create the container + if(!$('#THREAD_POOL_' + data.escapedName).length) { + // it doesn't exist so add it + var html = tmpl(htmlTemplateContainer, data); + // remove the loading thing first + $('#' + containerId + ' span.loading').remove(); + // get the current last column and remove the 'last' class from it + $('#' + containerId + ' div.last').removeClass('last'); + // now create the new data and add it + $('#' + containerId + '').append(html); + // add the 'last' class to the column we just added + $('#' + containerId + ' div.monitor').last().addClass('last'); + + // add the default sparkline graph + d3.selectAll('#graph_THREAD_POOL_' + data.escapedName + ' svg').append("svg:path"); + + // remember this is new so we can trigger a sort after setting data + addNew = true; + } + + // set the rate on the div element so it's available for sorting + $('#THREAD_POOL_' + data.escapedName).attr('rate_value', data.ratePerSecondPerHost); + + // now update/insert the data + $('#THREAD_POOL_' + data.escapedName + ' div.monitor_data').html(tmpl(htmlTemplate, data)); + + // set variables for circle visualization + var rate = data.ratePerSecondPerHost; + // we will treat each item in queue as 1% of an error visualization + // ie. 5 threads in queue per instance == 5% error percentage + var errorPercentage = data.currentQueueSize / data.reportingHosts; + + updateCircle('#THREAD_POOL_' + data.escapedName + ' circle', rate, errorPercentage); + + if(addNew) { + // sort since we added a new circuit + 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; // we are hardcoding to support only 1 decimal so that our padding logic at the end is simple + var result = Math.round(num*Math.pow(10,dec))/Math.pow(10,dec); + var resultAsString = result.toString(); + if(resultAsString.indexOf('.') == -1) { + resultAsString = resultAsString + '.'; + for(var i=0; i parseInt(maxXaxisForCircle)) { + newXaxisForCircle = maxXaxisForCircle; + } + var newYaxisForCircle = self.circleYaxis(rate); + if(parseInt(newYaxisForCircle) > parseInt(maxYaxisForCircle)) { + newYaxisForCircle = maxYaxisForCircle; + } + var newRadiusForCircle = self.circleRadius(rate); + if(parseInt(newRadiusForCircle) > parseInt(maxRadiusForCircle)) { + newRadiusForCircle = maxRadiusForCircle; + } + + d3.selectAll(cssTarget) + .transition() + .duration(400) + .attr("cy", newYaxisForCircle) + .attr("cx", newXaxisForCircle) + .attr("r", newRadiusForCircle) + .style("fill", self.colorRange(errorPercentage)); + } + + /* private */ function deleteThreadPool(poolName) { + $('#THREAD_POOL_' + poolName).remove(); + } + + } + + // public methods for sorting + HystrixThreadPoolMonitor.prototype.sortByVolume = function() { + var direction = "desc"; + if(this.sortedBy == 'rate_desc') { + direction = 'asc'; + } + this.sortByVolumeInDirection(direction); + } + + HystrixThreadPoolMonitor.prototype.sortByVolumeInDirection = function(direction) { + this.sortedBy = 'rate_' + direction; + $('#' + this.containerId + ' div.monitor').tsort({order: direction, attr: 'rate_value'}); + } + + HystrixThreadPoolMonitor.prototype.sortAlphabetically = function() { + var direction = "asc"; + if(this.sortedBy == 'alph_asc') { + direction = 'desc'; + } + this.sortAlphabeticalInDirection(direction); + } + + HystrixThreadPoolMonitor.prototype.sortAlphabeticalInDirection = function(direction) { + this.sortedBy = 'alph_' + direction; + $('#' + this.containerId + ' div.monitor').tsort("p.name", {order: direction}); + } + + HystrixThreadPoolMonitor.prototype.sortByMetricInDirection = function(direction, metric) { + $('#' + this.containerId + ' div.monitor').tsort(metric, {order: direction}); + } + + // this method is for when new divs are added to cause the elements to be sorted to whatever the user last chose + HystrixThreadPoolMonitor.prototype.sortSameAsLast = function() { + if(this.sortedBy == 'alph_asc') { + this.sortAlphabeticalInDirection('asc'); + } else if(this.sortedBy == 'alph_desc') { + this.sortAlphabeticalInDirection('desc'); + } else if(this.sortedBy == 'rate_asc') { + this.sortByVolumeInDirection('asc'); + } else if(this.sortedBy == 'rate_desc') { + this.sortByVolumeInDirection('desc'); + } else if(this.sortedBy == 'error_asc') { + this.sortByErrorInDirection('asc'); + } else if(this.sortedBy == 'error_desc') { + this.sortByErrorInDirection('desc'); + } else if(this.sortedBy == 'lat90_asc') { + this.sortByMetricInDirection('asc', 'p90'); + } else if(this.sortedBy == 'lat90_desc') { + this.sortByMetricInDirection('desc', 'p90'); + } else if(this.sortedBy == 'lat99_asc') { + this.sortByMetricInDirection('asc', 'p99'); + } else if(this.sortedBy == 'lat99_desc') { + this.sortByMetricInDirection('desc', 'p99'); + } else if(this.sortedBy == 'lat995_asc') { + this.sortByMetricInDirection('asc', 'p995'); + } else if(this.sortedBy == 'lat995_desc') { + this.sortByMetricInDirection('desc', 'p995'); + } else if(this.sortedBy == 'latMean_asc') { + this.sortByMetricInDirection('asc', 'pMean'); + } else if(this.sortedBy == 'latMean_desc') { + this.sortByMetricInDirection('desc', 'pMean'); + } else if(this.sortedBy == 'latMedian_asc') { + this.sortByMetricInDirection('asc', 'pMedian'); + } else if(this.sortedBy == 'latMedian_desc') { + this.sortByMetricInDirection('desc', 'pMedian'); + } + } + + // default sort type and direction + this.sortedBy = 'alph_asc'; + + + // a temporary home for the logger until we become more sophisticated + function log(message) { + console.log(message); + }; + + function addCommas(nStr){ + nStr += ''; + if(nStr.length <=3) { + return nStr; //shortcut if we don't need commas + } + x = nStr.split('.'); + x1 = x[0]; + x2 = x.length > 1 ? '.' + x[1] : ''; + var rgx = /(\d+)(\d{3})/; + while (rgx.test(x1)) { + x1 = x1.replace(rgx, '$1' + ',' + '$2'); + } + return x1 + x2; + } +})(window) + + diff --git a/spring-cloud-netflix-core/src/main/resources/static/hystrix/components/hystrixThreadPool/templates/hystrixThreadPool.html b/spring-cloud-netflix-core/src/main/resources/static/hystrix/components/hystrixThreadPool/templates/hystrixThreadPool.html new file mode 100644 index 00000000..1e653ccb --- /dev/null +++ b/spring-cloud-netflix-core/src/main/resources/static/hystrix/components/hystrixThreadPool/templates/hystrixThreadPool.html @@ -0,0 +1,33 @@ + +
+ +
+ Host: <%= addCommas(ratePerSecondPerHost) %>/s +
+
+ Cluster: <%= addCommas(ratePerSecond) %>/s +
+ +
+ +
+
Active
+
<%= currentActiveCount%>
+ +
Max Active
+
<%= addCommas(rollingMaxActiveThreads)%>
+
+ +
+
Queued
+
<%= currentQueueSize %>
+
Executions
+
<%= addCommas(rollingCountThreadsExecuted)%>
+
+
+
Pool Size
+
<%= currentPoolSize %>
+
Queue Size
+
<%= propertyValue_queueSizeRejectionThreshold %>
+
+ \ No newline at end of file diff --git a/spring-cloud-netflix-core/src/main/resources/static/hystrix/components/hystrixThreadPool/templates/hystrixThreadPoolContainer.html b/spring-cloud-netflix-core/src/main/resources/static/hystrix/components/hystrixThreadPool/templates/hystrixThreadPoolContainer.html new file mode 100644 index 00000000..6af0ef2c --- /dev/null +++ b/spring-cloud-netflix-core/src/main/resources/static/hystrix/components/hystrixThreadPool/templates/hystrixThreadPoolContainer.html @@ -0,0 +1,34 @@ +
+ + <% + var displayName = name; + var toolTip = ""; + if(displayName.length > 32) { + displayName = displayName.substring(0,4) + "..." + displayName.substring(displayName.length-20, displayName.length); + toolTip = "title=\"" + name + "\""; + } + %> + +
+

><%= displayName %>

+
+
+
+ + + + +
diff --git a/spring-cloud-netflix-core/src/main/resources/static/hystrix/css/global.css b/spring-cloud-netflix-core/src/main/resources/static/hystrix/css/global.css new file mode 100644 index 00000000..46898b11 --- /dev/null +++ b/spring-cloud-netflix-core/src/main/resources/static/hystrix/css/global.css @@ -0,0 +1,71 @@ +@IMPORT url("resets.css"); + +body { + font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; +} + +img, object, embed { + max-width: 100%; +} + +img { + height: auto; +} + + +#header { + background: #FFFFFF url(http://raw.github.com/wiki/Netflix/Hystrix/images/hystrix-logo-tagline-tiny.png) no-repeat scroll 99% 0%; + height: 65px; + margin-bottom: 5px; +} + +#header h2 { + float:left; + color: black; + position:relative; + padding-left: 20px; + top: 26px; + font-size: 20px; + font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; +} + +#header .header_nav { + position:absolute; + top:48px; + right:15px; +} + +#header .header_links { + float:left; + color: lightgray; + font-size: 18px; + top: 3px; + padding-left: 10px; + font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; +} + +#header .header_links a { + color: white; +} + +#header .header_clusters { + float:left; + position:relative; + padding-left: 10px; + top: -1px; + font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; +} + + +@media screen and (min-width: 1500px) { + + #header .header_nav { + top:13px; + right:130px; + } + + #header { + background: #FFFFFF url(http://raw.github.com/wiki/Netflix/Hystrix/images/hystrix-logo-tagline-tiny.png) no-repeat scroll 99% 50%; + height: 65px; + } +} diff --git a/spring-cloud-netflix-core/src/main/resources/static/hystrix/css/resets.css b/spring-cloud-netflix-core/src/main/resources/static/hystrix/css/resets.css new file mode 100644 index 00000000..4d137c7e --- /dev/null +++ b/spring-cloud-netflix-core/src/main/resources/static/hystrix/css/resets.css @@ -0,0 +1,102 @@ +/* +html5doctor.com Reset Stylesheet +v1.6.1 +Last Updated: 2010-09-17 +Author: Richard Clark - http://richclarkdesign.com +Twitter: @rich_clark +*/ + +html, body, div, span, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +abbr, address, cite, code, +del, dfn, em, img, ins, kbd, q, samp, +small, strong, sub, sup, var, +b, i, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td, +article, aside, canvas, details, figcaption, figure, +footer, header, hgroup, menu, nav, section, summary, +time, mark, audio, video { + margin:0; + padding:0; + border:0; + outline:0; + font-size:100%; + vertical-align:baseline; + background:transparent; +} + +body { + line-height:1; +} + +article,aside,details,figcaption,figure, +footer,header,hgroup,menu,nav,section { + display:block; +} + +nav ul { + list-style:none; +} + +blockquote, q { + quotes:none; +} + +blockquote:before, blockquote:after, +q:before, q:after { + content:''; + content:none; +} + +a { + margin:0; + padding:0; + font-size:100%; + vertical-align:baseline; + background:transparent; +} + +/* change colours to suit your needs */ +ins { + background-color:#ff9; + color:#000; + text-decoration:none; +} + +/* change colours to suit your needs */ +mark { + background-color:#ff9; + color:#000; + font-style:italic; + font-weight:bold; +} + +del { + text-decoration: line-through; +} + +abbr[title], dfn[title] { + border-bottom:1px dotted; + cursor:help; +} + +table { + border-collapse:collapse; + border-spacing:0; +} + +/* change border colour to suit your needs */ +hr { + display:block; + height:1px; + border:0; + border-top:1px solid #cccccc; + margin:1em 0; + padding:0; +} + +input, select { + vertical-align:middle; +} \ No newline at end of file diff --git a/spring-cloud-netflix-core/src/main/resources/static/hystrix/css/simplegrid/1236_grid.css b/spring-cloud-netflix-core/src/main/resources/static/hystrix/css/simplegrid/1236_grid.css new file mode 100644 index 00000000..052ee488 --- /dev/null +++ b/spring-cloud-netflix-core/src/main/resources/static/hystrix/css/simplegrid/1236_grid.css @@ -0,0 +1,21 @@ +/* SimpleGrid - a fork of CSSGrid by Crowd Favorite (https://github.com/crowdfavorite/css-grid) + * http://simplegrid.info + * by Conor Muirhead (http://conor.cc) of Early LLC (http://earlymade.com) + * License: http://creativecommons.org/licenses/MIT/ */ + +/* Containers */ +body { font-size: 1.125em; } +.grid{ width:1206px; } + +/* 6-Col Grid Sizes */ +.slot-0,.slot-1,.slot-2,.slot-3,.slot-4,.slot-5{ width:176px; } /* Sixths */ +.slot-0-1,.slot-1-2,.slot-2-3,.slot-3-4,.slot-4-5{ width:382px; } /* Thirds */ +.slot-0-1-2-3,.slot-1-2-3-4,.slot-2-3-4-5{ width:794px; } /* Two-Thirds */ +.slot-0-1-2-3-4,.slot-1-2-3-4-5{ width:1000px; } /* Five-Sixths */ + +/* 4-Col Grid Sizes */ +.slot-6,.slot-7,.slot-8,.slot-9{ width:279px; } /* Quarters */ +.slot-6-7-8,.slot-7-8-9{ width:897px; } /* Three-Quarters */ + +/* 6-Col/4-Col Shared Grid Sizes */ +.slot-0-1-2,.slot-1-2-3,.slot-2-3-4,.slot-3-4-5, .slot-6-7,.slot-7-8,.slot-8-9{ width:588px; } /* Halves */ \ No newline at end of file diff --git a/spring-cloud-netflix-core/src/main/resources/static/hystrix/css/simplegrid/720_grid.css b/spring-cloud-netflix-core/src/main/resources/static/hystrix/css/simplegrid/720_grid.css new file mode 100644 index 00000000..0ef2432c --- /dev/null +++ b/spring-cloud-netflix-core/src/main/resources/static/hystrix/css/simplegrid/720_grid.css @@ -0,0 +1,33 @@ +/* SimpleGrid - a fork of CSSGrid by Crowd Favorite (https://github.com/crowdfavorite/css-grid) + * http://simplegrid.info + * by Conor Muirhead (http://conor.cc) of Early LLC (http://earlymade.com) + * License: http://creativecommons.org/licenses/MIT/ */ + +/* Containers */ +body { font-size: 0.875em; padding: 0; } +.grid{ margin:0 auto; padding: 0 10px; width:700px; } +.row{ clear:left; } + +/* Slots Setup */ +.slot-0,.slot-1,.slot-2,.slot-3,.slot-4,.slot-5,.slot-0-1,.slot-0-1-2,.slot-0-1-2-3,.slot-0-1-2-3-4,.slot-0-1-2-3-4-5,.slot-1-2,.slot-1-2-3,.slot-1-2-3-4,.slot-1-2-3-4-5,.slot-2-3,.slot-2-3-4,.slot-2-3-4-5,.slot-3-4,.slot-3-4-5,.slot-4-5,.slot-6,.slot-7,.slot-8,.slot-9,.slot-6-7,.slot-6-7-8,.slot-6-7-8-9,.slot-7-8,.slot-7-8-9,.slot-8-9{ display:inline; float:left; margin-left:20px; } + +/* 6-Col Grid Sizes */ +.slot-0,.slot-1,.slot-2,.slot-3,.slot-4,.slot-5{ width:100px; } /* Sixths */ +.slot-0-1,.slot-1-2,.slot-2-3,.slot-3-4,.slot-4-5{ width:220px; } /* Thirds */ +.slot-0-1-2-3,.slot-1-2-3-4,.slot-2-3-4-5{ width:460px; } /* Two-Thirds */ +.slot-0-1-2-3-4,.slot-1-2-3-4-5{ width:580px; } /* Five-Sixths */ + +/* 4-Col Grid Sizes */ +.slot-6,.slot-7,.slot-8,.slot-9{ width:160px; } /* Quarters */ +.slot-6-7-8,.slot-7-8-9{ width:520px; } /* Three-Quarters */ + +/* 6-Col/4-Col Shared Grid Sizes */ +.slot-0-1-2,.slot-1-2-3,.slot-2-3-4,.slot-3-4-5, .slot-6-7,.slot-7-8,.slot-8-9{ width:340px; } /* Halves */ +.slot-0-1-2-3-4-5, .slot-6-7-8-9{ width: 100%; } /* Full-Width */ + +/* Zeroing Out Leftmost Slot Margins */ +.slot-0,.slot-0-1,.slot-0-1-2,.slot-0-1-2-3,.slot-0-1-2-3-4,.slot-0-1-2-3-4-5,.slot-6,.slot-6-7,.slot-6-7-8,.slot-6-7-8-9,.slot-1 .slot-1,.slot-1-2 .slot-1,.slot-1-2 .slot-1-2,.slot-1-2-3 .slot-1,.slot-1-2-3 .slot-1-2,.slot-1-2-3 .slot-1-2-3,.slot-1-2-3-4 .slot-1,.slot-1-2-3-4 .slot-1-2,.slot-1-2-3-4 .slot-1-2-3,.slot-1-2-3-4 .slot-1-2-3-4,.slot-1-2-3-4-5 .slot-1,.slot-1-2-3-4-5 .slot-1-2,.slot-1-2-3-4-5 .slot-1-2-3,.slot-1-2-3-4-5 .slot-1-2-3-4,.slot-1-2-3-4-5 .slot-1-2-3-4-5,.slot-2 .slot-2,.slot-2-3 .slot-2,.slot-2-3 .slot-2-3,.slot-2-3-4 .slot-2,.slot-2-3-4 .slot-2-3,.slot-2-3-4 .slot-2-3-4,.slot-2-3-4-5 .slot-2,.slot-2-3-4-5 .slot-2-3,.slot-2-3-4-5 .slot-2-3-4,.slot-2-3-4-5 .slot-2-3-4-5,.slot-3 .slot-3,.slot-3-4 .slot-3,.slot-3-4 .slot-3-4,.slot-3-4-5 .slot-3,.slot-3-4-5 .slot-3-4,.slot-3-4-5 .slot-3-4-5,.slot-4 .slot-4,.slot-4-5 .slot-4,.slot-4-5 .slot-4-5,.slot-5 .slot-5,.slot-7 .slot-7,.slot-7-8 .slot-7,.slot-7-8 .slot-7-8,.slot-7-8-9 .slot-7,.slot-7-8-9 .slot-7-8,.slot-7-8-9 .slot-7-8-9,.slot-8 .slot-8,.slot-8-9 .slot-8,.slot-8-9 .slot-8-9{ margin-left:0 !important; } /* Important is to avoid repeating this in larger screen css files */ + +/* Row Clearfix */ +.row:after{ visibility:hidden; display:block; font-size:0; content:" "; clear:both; height:0; } +.row{ zoom:1; } \ No newline at end of file diff --git a/spring-cloud-netflix-core/src/main/resources/static/hystrix/css/simplegrid/986_grid.css b/spring-cloud-netflix-core/src/main/resources/static/hystrix/css/simplegrid/986_grid.css new file mode 100644 index 00000000..83a59788 --- /dev/null +++ b/spring-cloud-netflix-core/src/main/resources/static/hystrix/css/simplegrid/986_grid.css @@ -0,0 +1,24 @@ +/* SimpleGrid - a fork of CSSGrid by Crowd Favorite (https://github.com/crowdfavorite/css-grid) + * http://simplegrid.info + * by Conor Muirhead (http://conor.cc) of Early LLC (http://earlymade.com) + * License: http://creativecommons.org/licenses/MIT/ */ + +/* Containers */ +body { font-size: 100%; } +.grid{ width:966px; } + +/* Slots Setup */ +.slot-0,.slot-1,.slot-2,.slot-3,.slot-4,.slot-5,.slot-0-1,.slot-0-1-2,.slot-0-1-2-3,.slot-0-1-2-3-4,.slot-0-1-2-3-4-5,.slot-1-2,.slot-1-2-3,.slot-1-2-3-4,.slot-1-2-3-4-5,.slot-2-3,.slot-2-3-4,.slot-2-3-4-5,.slot-3-4,.slot-3-4-5,.slot-4-5,.slot-6,.slot-7,.slot-8,.slot-9,.slot-6-7,.slot-6-7-8,.slot-6-7-8-9,.slot-7-8,.slot-7-8-9,.slot-8-9{ display:inline; float:left; margin-left:30px; } + +/* 6-Col Grid Sizes */ +.slot-0,.slot-1,.slot-2,.slot-3,.slot-4,.slot-5{ width:136px; } /* Sixths */ +.slot-0-1,.slot-1-2,.slot-2-3,.slot-3-4,.slot-4-5{ width:302px; } /* Thirds */ +.slot-0-1-2-3,.slot-1-2-3-4,.slot-2-3-4-5{ width:634px; } /* Two-Thirds */ +.slot-0-1-2-3-4,.slot-1-2-3-4-5{ width:800px; } /* Five-Sixths */ + +/* 4-Col Grid Sizes */ +.slot-6,.slot-7,.slot-8,.slot-9{ width:219px; } /* Quarters */ +.slot-6-7-8,.slot-7-8-9{ width:717px; } /* Three-Quarters */ + +/* 6-Col/4-Col Shared Grid Sizes */ +.slot-0-1-2,.slot-1-2-3,.slot-2-3-4,.slot-3-4-5, .slot-6-7,.slot-7-8,.slot-8-9{ width:468px; } /* Halves */ \ No newline at end of file diff --git a/spring-cloud-netflix-core/src/main/resources/static/hystrix/css/simplegrid/LICENSE.txt b/spring-cloud-netflix-core/src/main/resources/static/hystrix/css/simplegrid/LICENSE.txt new file mode 100644 index 00000000..e942914a --- /dev/null +++ b/spring-cloud-netflix-core/src/main/resources/static/hystrix/css/simplegrid/LICENSE.txt @@ -0,0 +1,19 @@ +Copyright (c) 2011 Crowd Favorite, Ltd. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/spring-cloud-netflix-core/src/main/resources/static/hystrix/css/simplegrid/README.txt b/spring-cloud-netflix-core/src/main/resources/static/hystrix/css/simplegrid/README.txt new file mode 100644 index 00000000..15cfc8c3 --- /dev/null +++ b/spring-cloud-netflix-core/src/main/resources/static/hystrix/css/simplegrid/README.txt @@ -0,0 +1 @@ +http://simplegrid.info/ \ No newline at end of file diff --git a/spring-cloud-netflix-core/src/main/resources/static/hystrix/css/simplegrid/percentage_grid.css b/spring-cloud-netflix-core/src/main/resources/static/hystrix/css/simplegrid/percentage_grid.css new file mode 100644 index 00000000..13cb4b2d --- /dev/null +++ b/spring-cloud-netflix-core/src/main/resources/static/hystrix/css/simplegrid/percentage_grid.css @@ -0,0 +1,27 @@ +/* Extension of SimpleGrid by benjchristensen to allow percentage based sizing on very large displays + * + * SimpleGrid - a fork of CSSGrid by Crowd Favorite (https://github.com/crowdfavorite/css-grid) + * http://simplegrid.info + * by Conor Muirhead (http://conor.cc) of Early LLC (http://earlymade.com) + * License: http://creativecommons.org/licenses/MIT/ */ + +/* Containers */ +body { font-size: 1.125em; } +.grid{ width:100%; } + +/* Slots Setup */ +.slot-0,.slot-1,.slot-2,.slot-3,.slot-4,.slot-5,.slot-0-1,.slot-0-1-2,.slot-0-1-2-3,.slot-0-1-2-3-4,.slot-0-1-2-3-4-5,.slot-1-2,.slot-1-2-3,.slot-1-2-3-4,.slot-1-2-3-4-5,.slot-2-3,.slot-2-3-4,.slot-2-3-4-5,.slot-3-4,.slot-3-4-5,.slot-4-5,.slot-6,.slot-7,.slot-8,.slot-9,.slot-6-7,.slot-6-7-8,.slot-6-7-8-9,.slot-7-8,.slot-7-8-9,.slot-8-9{ display:inline; float:left; margin-left:0px; } + + +/* 6-Col Grid Sizes */ +.slot-0,.slot-1,.slot-2,.slot-3,.slot-4,.slot-5{ width:16.6%; } /* Sixths */ +.slot-0-1,.slot-1-2,.slot-2-3,.slot-3-4,.slot-4-5{ width:33.3%; } /* Thirds */ +.slot-0-1-2-3,.slot-1-2-3-4,.slot-2-3-4-5{ width:66.6%; } /* Two-Thirds */ +.slot-0-1-2-3-4,.slot-1-2-3-4-5{ width:83.3%; } /* Five-Sixths */ + +/* 4-Col Grid Sizes */ +.slot-6,.slot-7,.slot-8,.slot-9{ width:25%; } /* Quarters */ +.slot-6-7-8,.slot-7-8-9{ width:75%; } /* Three-Quarters */ + +/* 6-Col/4-Col Shared Grid Sizes */ +.slot-0-1-2,.slot-1-2-3,.slot-2-3-4,.slot-3-4-5, .slot-6-7,.slot-7-8,.slot-8-9{ width:50%; } /* Halves */ \ No newline at end of file diff --git a/spring-cloud-netflix-core/src/main/resources/static/hystrix/index.html b/spring-cloud-netflix-core/src/main/resources/static/hystrix/index.html new file mode 100644 index 00000000..1c0bc56b --- /dev/null +++ b/spring-cloud-netflix-core/src/main/resources/static/hystrix/index.html @@ -0,0 +1,56 @@ + + + + +Hystrix Dashboard + + + + + + + +
+ +
+ +
+
+ +

Hystrix Dashboard

+ +

+ Cluster via Turbine (default cluster): http://turbine-hostname:port/turbine.stream +
+ Cluster via Turbine (custom cluster): http://turbine-hostname:port/turbine.stream?cluster=[clusterName] +
+ Single Hystrix App: http://hystrix-app:port/hystrix.stream +

+ Delay: ms +      + Title:
+
+ +

+
+ +
+
+ + \ No newline at end of file diff --git a/spring-cloud-netflix-core/src/main/resources/static/hystrix/js/jquery.tinysort.min.js b/spring-cloud-netflix-core/src/main/resources/static/hystrix/js/jquery.tinysort.min.js new file mode 100644 index 00000000..40d62eb5 --- /dev/null +++ b/spring-cloud-netflix-core/src/main/resources/static/hystrix/js/jquery.tinysort.min.js @@ -0,0 +1,12 @@ +/* +* jQuery TinySort - A plugin to sort child nodes by (sub) contents or attributes. +* +* Version: 1.0.5 +* +* Copyright (c) 2008-2011 Ron Valstar http://www.sjeiti.com/ +* +* Dual licensed under the MIT and GPL licenses: +* http://www.opensource.org/licenses/mit-license.php +* http://www.gnu.org/licenses/gpl.html +*/ +(function(b){b.tinysort={id:"TinySort",version:"1.0.5",copyright:"Copyright (c) 2008-2011 Ron Valstar",uri:"http://tinysort.sjeiti.com/",defaults:{order:"asc",attr:"",place:"start",returns:false,useVal:false}};b.fn.extend({tinysort:function(h,j){if(h&&typeof(h)!="string"){j=h;h=null}var e=b.extend({},b.tinysort.defaults,j);var p={};this.each(function(t){var v=(!h||h=="")?b(this):b(this).find(h);var u=e.order=="rand"?""+Math.random():(e.attr==""?(e.useVal?v.val():v.text()):v.attr(e.attr));var s=b(this).parent();if(!p[s]){p[s]={s:[],n:[]}}if(v.length>0){p[s].s.push({s:u,e:b(this),n:t})}else{p[s].n.push({e:b(this),n:t})}});for(var g in p){var d=p[g];d.s.sort(function k(t,s){var i=t.s.toLowerCase?t.s.toLowerCase():t.s;var u=s.s.toLowerCase?s.s.toLowerCase():s.s;if(c(t.s)&&c(s.s)){i=parseFloat(t.s);u=parseFloat(s.s)}return(e.order=="asc"?1:-1)*(iu?1:0))})}var m=[];for(var g in p){var d=p[g];var n=[];var f=b(this).length;switch(e.place){case"first":b.each(d.s,function(s,t){f=Math.min(f,t.n)});break;case"org":b.each(d.s,function(s,t){n.push(t.n)});break;case"end":f=d.n.length;break;default:f=0}var q=[0,0];for(var l=0;l=f&&l0?d[1]:false}function a(e,f){var d=false;b.each(e,function(h,g){if(!d){d=g==f}});return d}b.fn.TinySort=b.fn.Tinysort=b.fn.tsort=b.fn.tinysort})(jQuery); \ No newline at end of file diff --git a/spring-cloud-netflix-core/src/main/resources/static/hystrix/js/tmpl.js b/spring-cloud-netflix-core/src/main/resources/static/hystrix/js/tmpl.js new file mode 100644 index 00000000..d7098f78 --- /dev/null +++ b/spring-cloud-netflix-core/src/main/resources/static/hystrix/js/tmpl.js @@ -0,0 +1,43 @@ + +//Simple JavaScript Templating +//John Resig - http://ejohn.org/ - MIT Licensed +// http://ejohn.org/blog/javascript-micro-templating/ +(function(window, undefined) { + var cache = {}; + + window.tmpl = function tmpl(str, data) { + try { + // Figure out if we're getting a template, or if we need to + // load the template - and be sure to cache the result. + var fn = !/\W/.test(str) ? + cache[str] = cache[str] || + tmpl(document.getElementById(str).innerHTML) : + + // Generate a reusable function that will serve as a template + // generator (and which will be cached). + new Function("obj", + "var p=[],print=function(){p.push.apply(p,arguments);};" + + + // Introduce the data as local variables using with(){} + "with(obj){p.push('" + + + // Convert the template into pure JavaScript + str + .replace(/[\r\t\n]/g, " ") + .split("<%").join("\t") + .replace(/((^|%>)[^\t]*)'/g, "$1\r") + .replace(/\t=(.*?)%>/g, "',$1,'") + .split("\t").join("');") + .split("%>").join("p.push('") + .split("\r").join("\\'") + + "');}return p.join('');"); + + //console.log(fn); + + // Provide some basic currying to the user + return data ? fn(data) : fn; + }catch(e) { + console.log(e); + } + }; +})(window); diff --git a/spring-cloud-netflix-core/src/main/resources/static/hystrix/monitor/monitor.css b/spring-cloud-netflix-core/src/main/resources/static/hystrix/monitor/monitor.css new file mode 100644 index 00000000..a38647f0 --- /dev/null +++ b/spring-cloud-netflix-core/src/main/resources/static/hystrix/monitor/monitor.css @@ -0,0 +1,101 @@ +.container { + padding-left: 20px; + padding-right: 20px; +} + +.row { + width: 100%; + margin: 0 auto; + overflow: hidden; +} + +.spacer { + width: 100%; + margin: 0 auto; + padding-top:4px; + clear:both; +} + + +.last { + margin-right: 0px; +} + +.menubar { + overflow: hidden; + border-bottom: 1px solid black; +} + +.menubar div { + padding-bottom:5px; + + margin: 0 auto; + overflow: hidden; + + font-size: 80%; + font-family:'Bookman Old Style',Bookman,'URW Bookman L','Palatino Linotype',serif; + + float:left; +} + +.menubar .title { + float: left; + padding-right: 20px; + + font-size: 110%; + font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; + font-weight: bold; + + vertical-align: bottom; +} + +.menubar .menu_actions { + float: left; + position:relative; + top: 4px; +} + +.menubar .menu_legend { + float: right; + position:relative; + top: 4px; + +} + +h3.sectionHeader { + color: black; + font-size: 110%; + padding-top: 4px; + padding-bottom: 4px; + padding-left: 8px; + font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; + background: lightgrey; +} + +.success { + color: green; +} +.shortCircuited { + color: blue; +} +.timeout { + color: #FF9900; /* shade of orange */ +} +.failure { + color: red; +} + +.rejected { + color: purple; +} + +.exceptionsThrown { + color: brown; +} + +@media screen and (max-width: 1100px) { + .container { + padding-left: 5px; + padding-right: 5px; + } +} diff --git a/spring-cloud-netflix-core/src/main/resources/static/hystrix/monitor/monitor.html b/spring-cloud-netflix-core/src/main/resources/static/hystrix/monitor/monitor.html new file mode 100644 index 00000000..2444f4ee --- /dev/null +++ b/spring-cloud-netflix-core/src/main/resources/static/hystrix/monitor/monitor.html @@ -0,0 +1,197 @@ + + + + + Hystrix Monitor + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+
Loading ...
+ +
+
+ +
+ +
+
Loading ...
+
+ + + + + + + +