From 2bf746736bce59f2b81c4a13757bc0f5e0e43d98 Mon Sep 17 00:00:00 2001 From: Richard Wall Date: Wed, 23 Jun 2010 23:24:21 +0100 Subject: Rename stuff to jarmon --- jarmon.js | 773 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 773 insertions(+) create mode 100644 jarmon.js (limited to 'jarmon.js') diff --git a/jarmon.js b/jarmon.js new file mode 100644 index 0000000..67b4522 --- /dev/null +++ b/jarmon.js @@ -0,0 +1,773 @@ +/* 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