/* Copyright (c) 2010 Richard Wall * See LICENSE for details. * * Wrappers and convenience fuctions for working with the javascriptRRD, jQuery, * and flot charting packages. * * Designed to work well with the RRD files generated by Collectd: * - http://collectd.org/ * * Requirements: * - JavascriptRRD: http://javascriptrrd.sourceforge.net/ * - jQuery: http://jquery.com/ * - Flot: http://code.google.com/p/flot/ * - MochiKit.Async: http://www.mochikit.com/ */ if(typeof jarmon == 'undefined') { var jarmon = {}; } /** * Download a binary file asynchronously using the jQuery.ajax function * * @param url: The url of the object to be downloaded * @return: A I{MochiKit.Async.Deferred} which will callback with an instance of * I{javascriptrrd.BinaryFile} **/ jarmon.downloadBinary = function(url) { var d = new MochiKit.Async.Deferred(); $.ajax({ url: url, dataType: 'text', cache: false, beforeSend: function(request) { try { request.overrideMimeType('text/plain; charset=x-user-defined'); } catch(e) { // IE doesn't support overrideMimeType } }, success: function(data) { try { d.callback(new BinaryFile(data)); } catch(e) { d.errback(e); } }, error: function(xhr, textStatus, errorThrown) { // Special case for IE which handles binary data slightly // differently. if(textStatus == 'parsererror') { if (typeof xhr.responseBody != 'undefined') { return this.success(xhr.responseBody); } } d.errback(new Error(xhr.status)); } }); return d; }; /** * A wrapper around an instance of javascriptrrd.RRDFile which provides a * convenient way to query the RRDFile based on time range, RRD data source (DS) * and RRD consolidation function (CF). * * @param startTime: A javascript {Date} instance representing the start of query * time range, or {null} to return earliest available data. * @param endTime: A javascript {Date} instance representing the end of query * time range, or {null} to return latest available data. * @param dsId: A {String} name of an RRD DS or an {Int} DS index number or * {null} to return the first available DS. * @param cfName: A {String} name of an RRD consolidation function * @return: A flot compatible data series object **/ jarmon.RrdQuery = function(rrd, unit) { this.rrd = rrd; this.unit = unit; }; jarmon.RrdQuery.prototype.getData = function(startTime, endTime, dsId, cfName) { /** * Generate a Flot compatible data object containing rows between start and * end time. The rows are taken from the first RRA whose data spans the * requested time range. * * @param startTime: The I{Date} start time * @param endTime: The I{Date} end time * @param dsId: An index I{Number} or key I{String} identifying the RRD * datasource (DS). * @param cfName: The name I{String} of an RRD consolidation function (CF) * eg AVERAGE, MIN, MAX * @return: A Flot compatible data series I{Object} * eg {label:'', data:[], unit: ''} **/ var startTimestamp = startTime.getTime()/1000; var lastUpdated = this.rrd.getLastUpdate(); var endTimestamp = lastUpdated; if(endTime) { endTimestamp = endTime.getTime()/1000; // If end time stamp is beyond the range of this rrd then reset it if(lastUpdated < endTimestamp) { endTimestamp = lastUpdated; } } if(dsId == null) { dsId = 0; } var ds = this.rrd.getDS(dsId); if(cfName == null) { cfName = 'AVERAGE'; } var rra, step, rraRowCount, firstUpdated; for(var i=0; i -1 && this.lastUpdate < endTimestamp )) { this._download = jarmon.downloadBinary(this.url) .addCallback( function(self, binary) { // Upon successful download convert the resulting binary // into an RRD file and pass it on to the next callback // in the chain. var rrd = new RRDFile(binary); self.lastUpdate = rrd.getLastUpdate(); return rrd; }, this); } // Set up a deferred which will call getData on the local RrdQuery object // returning a flot compatible data object to the caller. var ret = new MochiKit.Async.Deferred().addCallback( function(self, startTime, endTime, dsId, rrd) { return new jarmon.RrdQuery(rrd, self.unit).getData(startTime, endTime, dsId); }, this, startTime, endTime, dsId); // Add a pair of callbacks to the current download which will callback the // result which we setup above. this._download.addBoth( function(ret, res) { if(res instanceof Error) { ret.errback(res); } else { ret.callback(res); } return res; }, ret); return ret; }; /** * Wraps a I{RrdQueryRemote} to provide access to a different RRD DSs within a * single RrdDataSource. * * @param rrdQuery: An I{RrdQueryRemote} * @param dsId: An index or keyname of an RRD DS **/ jarmon.RrdQueryDsProxy = function(rrdQuery, dsId) { this.rrdQuery = rrdQuery; this.dsId = dsId; this.unit = rrdQuery.unit; }; jarmon.RrdQueryDsProxy.prototype.getData = function(startTime, endTime) { /** * Call I{RrdQueryRemote.getData} with a particular dsId **/ return this.rrdQuery.getData(startTime, endTime, this.dsId); }; /** * A class for creating a Flot chart from a series of RRD Queries * * @param template: A I{jQuery} containing a single element into which the chart * will be drawn * @param options: An I{Object} containing Flot options which describe how the * chart should be drawn. **/ jarmon.Chart = function(template, options) { this.template = template; this.options = jQuery.extend(true, {yaxis: {}}, options); this.data = []; var self = this; // Listen for clicks on the legend items - onclick enable / disable the // corresponding data source. $('.legend .legendLabel', this.template[0]).live('click', function(e) { self.switchDataEnabled($(this).text()); self.draw(); }); this.options['yaxis']['ticks'] = function(axis) { /** * Choose a suitable SI multiplier based on the min and max values from * the axis and then generate appropriate yaxis tick labels. * * @param axis: An I{Object} with min and max properties * @return: An array of ~5 tick labels **/ var siPrefixes = { 0: '', 1: 'K', 2: 'M', 3: 'G', 4: 'T' } var si = 0; while(true) { if( Math.pow(1000, si+1)*0.9 > axis.max ) { break; } si++; } var minVal = axis.min/Math.pow(1000, si); var maxVal = axis.max/Math.pow(1000, si); var stepSizes = [0.01, 0.05, 0.1, 0.25, 0.5, 1, 5, 10, 25, 50, 100, 250]; var realStep = (maxVal - minVal)/5.0; var stepSize, decimalPlaces = 0; for(var i=0; i -1 ) { labelCell.addClass('disabled'); } } ); var yaxisUnitLabel = $('
').text(self.siPrefix + unit) .css({width: '100px', position: 'absolute', top: '80px', left: '-90px', 'text-align': 'right'}); self.template.append(yaxisUnitLabel); yaxisUnitLabel.position(self.template.position()); return data; }, this) .addErrback( function(self, failure) { self.template.text('error: ' + failure.message); }, this) .addBoth( function(self, res) { self.template.trigger('chart_loaded'); return res; }, this); }; jarmon.Chart.fromRecipe = function(rrdUrlList, recipes, templateFactory) { /** * A factory function to generate a list of I{Chart} from a list of recipes * and a list of available rrd files in collectd path format. * * @param rrdUrlList: A list of rrd download paths * @param recipes: A list of recipe objects * @param templateFactory: A callable which generates an html template for a * chart. **/ var rrdUrlBlob = rrdUrlList.join('\n') var charts = []; var dataDict = {}; var recipe, chartData, template, c, i, j, x, ds, label, rrd, unit, re, match; for(i=0; i 0) { template = templateFactory(); template.find('.title').text(recipe['title']); c = new jarmon.Chart(template.find('.chart'), recipe['options']); for(j=0; j lastUpdate) { lastUpdate = chartData[i][j].lastUpdated; } } } var ranges = { xaxis: { from: Math.max(startTime.getTime(), firstUpdate), to: Math.min(endTime.getTime(), lastUpdate) } }; // Add a suitable extended head and tail to preview graph time axis var HOUR = 1000 * 60 * 60; var DAY = HOUR * 24; var WEEK = DAY * 7; var MONTH = DAY * 31; var YEAR = DAY * 365; var periods = [HOUR, HOUR*6, HOUR*12, DAY, DAY*3, WEEK, WEEK*2, MONTH, MONTH*3, MONTH*6, YEAR]; var range = ranges.xaxis.to - ranges.xaxis.from; for(var i=0; i