function formatSeriesLabel(labels) { var host = labels[0], plugin = labels[1], instance = labels[2], metric = labels[3], name; if (plugin == "irq") { name = name.replace(/^/, 'irq ') } // Plugin specific labeling else if (plugin == "interface") { name = instance.replace(/^if_(.*)-(.*)/, '$2 $1') + ' (' + metric + ')' } else if (["processes", "memory"].contains(plugin) || plugin.test(/^cpu-\d+/) ) { name = instance.split('-')[1] } else if (plugin == "swap") { if (instance.test(/^swap_io/)) { name = instance.replace(/^swap_(\w*)-(.*)$/, '$1 $2') } if (instance.test(/^swap-/)) { name = instance.split('-')[1] } } else if (plugin == "load") { name = metric.replace(/^\((.*)\)$/, '$1') } else if (plugin.test(/^disk/)) { name = instance.replace(/^disk_/, '') + ' (' + metric + ')' } else if (["entropy","users"].contains(plugin)) { name = metric } else if (plugin == "uptime") { name = instance } else if (plugin == "ping") { if (instance.test(/^ping_/)) { name = instance.replace(/^ping_(.*)-(.*)$/, '$1 $2') } else { name = metric + ' ' + instance.split('-')[1] } } else if (plugin.test(/^vmem/)) { if (instance.test(/^vmpage_number-/)) { name = instance.replace(/^vmpage_number-(.*)$/, '$1').replace('_', ' ') } if (instance.test(/^vmpage_io/)) { name = instance.replace(/^vmpage_io-(.*)$/, '$1 ') + metric } if (instance.test(/^vmpage_faults/)) { name = metric.trim() == "minflt" ? 'minor' : 'major' name += ' faults' } if (instance.test(/^vmpage_action-/)) { name = instance.replace(/^vmpage_action-(.*)$/, '$1').replace('_', ' ') } } else if (plugin.test(/^tcpconns/)) { name = instance.split('-')[1].replace('_', ' ') } else if (plugin.test(/^tail/)) { name = plugin.split('-').slice(1).join('-') + ' ' name = instance.split('-').slice(1).join('-') } else if (plugin == "apache") { var stash = instance.split('_')[1] if (stash.test(/^scoreboard/)) { name = 'connections: ' + stash.split('-')[1] } else { name = stash } } else if ( plugin.test(/^curl_json/) ) { var stash = instance.split('-')[2]; var stash = stash.replace(/[-|_]/, ' '); name = stash } else { // Generic label building name = instance name = name.replace(plugin.split('-')[0], '') name += metric == "value" ? "" : " (" + metric + ")" name = name.replace(/^[-|_]*/, '') name = name.trim().replace(/^\((.*)\)$/, '$1') } return name.trim() } function formatValue(value, options) { var precision = options.precision, min = options.min, max = options.max; switch(true) { case (Math.abs(max) > 1125899906842624): var label = value / 1125899906842624, unit = 'P'; break case (Math.abs(max) > 1099511627776): var label = value / 1099511627776, unit = 'T'; break case (Math.abs(max) > 1073741824): var label = value / 1073741824, unit = 'G'; break case (Math.abs(max) > 1048576): var label = value / 1048576, unit = 'M'; break case (Math.abs(max) > 1024): var label = value / 1024, unit = 'K'; break default: var label = value, unit = ''; break } if (!(label)) { label = 0; } return label.format({decimals: precision, suffix: unit}) } function formatDate(d) { var datetime = new Date(d) return datetime.format("%Y-%m-%d %H:%M:%S UTC%z") } function formatPluginName(name) { if (name.test(/^curl_json/)) { name = name.split('-')[1].replace(/(-|_)/, ' '); } return name } /* * VisageBase() * * Base class for fetching data and setting graph options. * Should be used by other classes to build specialised graphing behaviour. * */ var VisageBase = new Class({ Implements: [ Options, Events ], options: { secureJSON: false, httpMethod: 'get', live: false }, initialize: function(element, host, plugin, options) { this.parentElement = element; this.options.host = host; this.options.plugin = plugin; this.query = window.location.search.slice(1).parseQueryString(); this.options = Object.merge(this.options, this.query); this.setOptions(options); this.requestData = new Object(); this.requestData.start = this.options.start; this.requestData.finish = this.options.finish; this.getData(); // calls graphData }, dataURL: function() { var url = ['data', this.options.host, this.options.plugin]; // if the data exists on another host (useful for embedding) if (this.options.baseurl) { url.unshift(this.options.baseurl.replace(/\/$/, '')) } // for specific plugin instances if (this.options.pluginInstance) { url.push(this.options.pluginInstance) } // if no url is specified if (!url[0].match(/http\:\/\//)) { url[0] = '/' + url[0] } var options = ''; if (this.options.percentiles) { if (this.options.percentiles.length > 0) { options = '?percentiles=true'; this.options.percentile95 = true; } } return url.join('/') + options; }, getData: function() { this.request = new Request.JSONP({ url: this.dataURL(), data: this.requestData, secure: this.options.secureJSON, method: this.options.httpMethod, onComplete: function(json) { this.graphData(json); }.bind(this), onFailure: function(header, value) { $(this.parentElement).set('html', header) }.bind(this) }); this.request.send(); }, title: function() { if ($chk(this.options.name)) { var title = this.options.name } else { var title = [ formatPluginName(this.options.plugin), 'on', this.options.host ].join(' ') } return title }, }); /* * VisageGraph() * * General purpose graph for rendering data from a single plugin * with multiple plugin instances. * * Builds upon VisageBase(). * */ var VisageGraph = new Class({ Extends: VisageBase, Implements: Chain, // assemble data to graph, then draw it graphData: function(data) { this.response = data this.buildDataStructures() this.lastStart = this.series[0].data[0][0] this.lastFinish = this.series[0].data.getLast()[0] switch(true) { case $defined(this.chart) && this.requestData['live']: this.series.each(function(series, index) { var point = series.data[1]; this.chart.series[index].addPoint(point, false); }, this); this.chart.redraw(); break; case $defined(this.chart): this.series.each(function(series, index) { this.chart.series[index].setData(series.data, false); }, this); /* Reset the zoom */ //this.chart.toolbar.remove('zoom'); this.chart.xAxis.concat(this.chart.yAxis).each(function(axis) { axis.setExtremes(null,null,false); }); this.chart.redraw(); this.drawPercentiles(this.chart) break; default: this.drawChart() this.drawPercentiles(this.chart) break; } }, buildDataStructures: function (data) { var series = this.series = [] var host = this.options.host var plugin = this.options.plugin var data = data ? data : this.response $each(data[host][plugin], function(instance, instanceName) { $each(instance, function(metric, metricName) { var start = metric.start, finish = metric.finish, interval = (finish - start) / metric.data.length; var data = metric.data.map(function(value, index) { var x = (start + index * interval) * 1000, y = value; return [ x, y ]; }); var set = { name: [ host, plugin, instanceName, metricName ], data: data, percentile95: metric.percentile_95 }; series.push(set) }, this); }, this); return series }, getSeriesMinMax: function(series) { var min, max; series.each(function(set) { values = set.data.map(function(point) { var value = point[1]; return value }); var setMin = values.min() var setMax = values.max() if ($chk(min)) { min = min > setMin ? setMin : min } else { min = setMin } if ($chk(max)) { max = max < setMax ? setMax : max } else { max = setMax } }); return {'min': min, 'max': max}; }, removePercentiles: function(chart) { var series = this.series; series.each(function(set) { chart.yAxis[0].removePlotLine('95e_' + set.name[2] + set.name[3]); }); }, drawPercentiles: function(chart) { var series = this.series; /* Get the maximum value across all sets. * Used later on to determine the decimal place in the label. */ meta = this.getSeriesMinMax(series); var min = meta.min, max = meta.max; series.each(function(set) { formattedValue = formatValue(set.percentile95, { 'precision': 2, 'min': min, 'max': max }); chart.yAxis[0].removePlotLine('95e_' + set.name[2] + set.name[3]); chart.yAxis[0].addPlotLine({ id: '95e_' + set.name[2] + set.name[3], value: set.percentile95, color: '#ff0000', width: 1, zIndex: 5, label: { text: '95e ' + set.name[3] + ": " + formattedValue } }) }); }, drawChart: function() { var series = this.series, title = this.title(), element = this.parentElement, ytitle = formatPluginName(this.options.plugin), min, max; /* Get the maximum value across all sets. * Used later on to determine the decimal place in the label. */ meta = this.getSeriesMinMax(series); var min = meta.min, max = meta.max; this.chart = new Highcharts.Chart({ series: series, chart: { renderTo: element, type: 'line', marginRight: 0, marginBottom: 60, zoomType: 'xy', height: 300, events: { load: function(e) { setInterval(function() { if (this.options.live) { var data = { 'start': this.lastFinish / 1000, 'finish': this.lastFinish / 1000 + 10, 'live': true }; this.requestData = data; // FIXME: for 95e plotLines - need to update them each data retrieval // but perhaps 'live' just sends incremental data and so won't cause // a recalculation of the 95e figures on the server side? this.getData() } }.bind(this), 10000); }.bind(this) } }, title: { text: title, style: { 'fontSize': '18px', 'fontWeight': 'bold', 'color': '#333333', 'font-family': 'Bitstream Vera Sans, Helvetica Neue, sans-serif', } }, /* colors: [ '#204a87', '#4e9a06', '#cc0000', '#5c3566', '#f57900', '#e9b96e', '#ad7fa8', '#888a85', '#8ae234', '#75507b', '#c17d11', '#729fcf', '#73d216', '#ef2929', '#edd400', '#8f5902', '#555753', '#fce94f', '#2e3436', '#babdb6', '#3465a4', '#a40000', '#c4a000', '#ce5c00', '#d3d7cf', '#fcaf3e', '#eeeeec', ], */ xAxis: { title: { text: null }, lineColor: "#aaa", tickColor: "#aaa", type: 'datetime', dateTimeLabelFormats: { second: '%H:%M:%S', minute: '%H:%M', hour: '%H:%M', day: '%d/%m', week: '%d/%m', month: '%m/%Y', year: '%Y' } }, yAxis: { title: { text: null }, startOnTick: false, minPadding: 0.065, endOnTick: false, gridLineColor: "#dddddd", labels: { formatter: function() { var precision = 1, value = formatValue(this.value, { 'precision': precision, 'min': min, 'max': max }); return value } }, }, plotOptions: { series: { shadow: false, lineWidth: 1, marker: { enabled: false, states: { hover: { enabled: true, radius: 4, }, }, }, states: { hover: { enabled: true, lineWidth: 1, }, } } }, tooltip: { formatter: function() { var tip; tip = '' tip += formatSeriesLabel(this.series.name).trim() tip += '' + ' -> ' tip += '' tip += formatValue(this.y, { 'precision': 2, 'min': min, 'max': max }) tip += '' tip += ' (' + this.y + ')' tip += '' tip += '' tip += '
' tip += '' tip += formatDate(this.x) tip += '' return tip } }, legend: { layout: 'horizontal', align: 'center', verticalAlign: 'top', y: 255, borderWidth: 0, floating: true, labelFormatter: function() { return formatSeriesLabel(this.name) }, itemStyle: { cursor: 'pointer', color: '#333333' }, itemHoverStyle: { color: '#888' } }, credits: { enabled: false } }); this.buildDateSelector(); }, buildDateSelector: function() { /* * container * \ * - form * \ * - select * \ * - option * | * - option */ var currentDate = new Date; var currentUnixTime = parseInt(currentDate.getTime() / 1000); var container = $(this.parentElement); var form = this.form = new Element('form', { 'method': 'get', 'styles': { 'text-align': 'right', }, 'events': { 'submit': function(e) { this.requestData = this.form.getElement('select').getSelected()[0].value.parseQueryString() /* Draw everything again. */ this.getData(); }.bind(this) } }); /* Select dropdown */ var select = this.select = new Element('select', { 'class': 'date timescale', 'styles': { 'margin-bottom': '3px', 'border': '1px solid #aaa', }, 'events': { 'change': function(e) { e.target.form.fireEvent('submit', e) } } }); /* Timescales available in the dropdown */ var timescales = new Hash({ '1 hour': 1, '2 hours': 2, '6 hours': 6, '12 hours': 12, '24 hours': 24, '3 days': 72, '7 days': 168, '2 weeks': 336, '1 month': 774, '3 months': 2322, '6 months': 4368, '1 year': 8760, '2 years': 17520 }); timescales.each(function(hour, label) { var current = this.currentTimePeriod == 'last {label}'.substitute({'label': label }); var value = "start={start}".substitute({'start': currentUnixTime - (hour * 3600)}); var html = 'last {label}'.substitute({'label': label }); var option = new Element('option', { 'html': html, 'value': value, 'selected': (current ? 'selected' : ''), }); select.grab(option) }); /* Calendar month timescales dropdown */ var monthlyTimescales = new Hash({ 'current month': 0, 'previous month': 1, 'two months ago': 2, 'three months ago': 3}); monthlyTimescales.each(function(monthsAgo, label) { var current = this.currentTimePeriod == label; var value = "start=" + (new Date().decrement('month', monthsAgo).set('date', 1).set('hr', 0).set('min', 0).set('sec', 0).getTime() / 1000); value += '&finish=' + (new Date().decrement('month', monthsAgo - 1).set('date', 1).set('hr', 0).set('min', 0).set('sec', 0).getTime() / 1000); var option = new Element('option', { 'html': label, 'value': value, 'selected': (current ? 'selected' : ''), }); select.grab(option) }); var liveToggler = this.liveToggler = new Element('input', { 'type': 'checkbox', 'id': this.parentElement + '-live', 'name': 'live', 'checked': this.options.live, 'disabled': this.options.percentile95, 'events': { 'click': function(e) { this.options.live = !this.options.live if (this.options.live) { // tell percentiles95Toggler to be unchecked if (this.options.percentile95) { this.percentile95Toggler.fireEvent('click'); } this.percentile95Toggler.set('disabled', true); } else { this.requestData.live = false; this.percentile95Toggler.set('disabled', false); e.target.form.fireEvent('submit', e) } }.bind(this) }, 'styles': { 'margin-right': '4px', 'cursor': 'pointer' } }); var liveLabel = new Element('label', { 'for': this.parentElement + '-live', 'html': 'Live', 'styles': { 'font-family': 'sans-serif', 'font-size': '11px', 'margin-right': '8px', 'cursor': 'pointer' } }); var percentile95Toggler = this.percentile95Toggler = new Element('input', { 'type': 'checkbox', 'id': this.parentElement + '-percentile95', 'name': 'percentile95', 'checked': this.options.percentile95, 'events': { 'click': function() { this.options.percentile95 = !this.options.percentile95; if (!(this.options.percentiles)) { this.options.percentiles = []; } if (this.options.percentile95) { this.options.percentiles.push('95'); if ((this.chart) && (this.series[0].percentile95)) { this.drawPercentiles(this.chart); } else { this.getData(); } // tell liveToggler to be unchecked if (this.options.live) { this.liveToggler.fireEvent('click'); } this.liveToggler.set('disabled', true); } else { // FIXME - when adding support for 5th and 50th percentiles // etc we'll need to switch the options.percentiles array // to a hash for easy enabling / disabling of percentiles this.options.percentiles = []; if (this.chart) { this.removePercentiles(this.chart); } else { this.getData(); } this.liveToggler.set('disabled', false); } }.bind(this) }, 'styles': { 'margin-right': '4px', 'cursor': 'pointer' } }); var percentile95Label = new Element('label', { 'for': this.parentElement + '-percentile95', 'html': '95th Percentile', 'styles': { 'font-family': 'sans-serif', 'font-size': '11px', 'margin-right': '8px', 'cursor': 'pointer' } }); var exportLink = new Element('a', { 'href': this.dataURL(), 'html': 'Export data', 'styles': { 'font-family': 'sans-serif', 'font-size': '11px', 'margin-right': '8px', 'color': '#2F5A92', }, 'events': { 'mouseover': function(e) { e.stop(); var url = e.target.get('href'), extremes = this.chart.xAxis[0].getExtremes(), options = { 'start': extremes.dataMin, 'finish': parseInt(extremes.dataMax) }; var options = new Hash(options).toQueryString(), baseurl = this.dataURL(), url = baseurl += '?' + options; e.target.set('href', url) }.bind(this) } }); form.grab(exportLink) form.grab(percentile95Toggler) form.grab(percentile95Label) form.grab(liveToggler) form.grab(liveLabel) form.grab(select) container.grab(form, 'top') }, setTimePeriodTo: function(selected) { var option = this.select.getElements('option').filter(function(opt) { return opt.text == selected.text })[0]; option.set('selected', 'selected'); this.form.fireEvent('submit') } }); // buildEmbedder: function() { // var pre = new Element('textarea', { // 'id': 'embedder', // 'class': 'embedder', // 'html': this.embedCode(), // 'styles': { // 'width': '500px', // 'padding': '3px' // } // }); // this.embedderContainer.grab(pre); // // var slider = new Fx.Slide(pre, { // duration: 200 // }); // // slider.hide(); // // var toggler = new Element('a', { // 'id': 'toggler', // 'class': 'toggler', // 'html': '(embed)', // 'href': '#', // 'styles': { // 'font-size': '0.7em', // } // }); // toggler.addEvent('click', function(e) { // e.stop(); // slider.toggle(); // }); // this.embedderTogglerContainer.grab(toggler); // }, // embedCode: function() { // baseurl = "{protocol}//{host}".substitute({'host': window.location.host, 'protocol': window.location.protocol}); // code = "".substitute({'baseurl': baseurl}); // code += "
" // code += "" // return code.replace('<', '<').replace('>', '>') // },