├── test ├── invalid.xml ├── .gitignore ├── valid.rrd └── rrd-test.coffee ├── rrdRecord.coffee ├── rrdRecord.js ├── package.json ├── README.md ├── rrd.coffee └── rrd.js /test/invalid.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/.gitignore: -------------------------------------------------------------------------------- 1 | create-test.rrd 2 | empty-and-invalid.rrd 3 | -------------------------------------------------------------------------------- /test/valid.rrd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plainlystated/coffeescript-rrd/HEAD/test/valid.rrd -------------------------------------------------------------------------------- /rrdRecord.coffee: -------------------------------------------------------------------------------- 1 | class RRDRecord 2 | constructor: (@timestamp, @fieldNames) -> 3 | 4 | exports.RRDRecord = RRDRecord 5 | -------------------------------------------------------------------------------- /rrdRecord.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var RRDRecord; 3 | RRDRecord = (function() { 4 | function RRDRecord(timestamp, fieldNames) { 5 | this.timestamp = timestamp; 6 | this.fieldNames = fieldNames; 7 | } 8 | return RRDRecord; 9 | })(); 10 | exports.RRDRecord = RRDRecord; 11 | }).call(this); 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rrd" 3 | , "description": "A library for querying and manipulating a Round Robin Database" 4 | , "keywords": ["rrd", "round robin", "database"] 5 | , "author": "Patrick Schless (http://www.plainlystated.com)" 6 | , "version": "1.0.1" 7 | , "main": "./rrd.js" 8 | , "files": ["rrd.js", "rrd.coffee", "rrdRecord.js", "rrdRecord.coffee", "test"] 9 | , "repository" : { 10 | "type" : "git" 11 | , "url" : "https://github.com/plainlystated/coffeescript-rrd" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## RRD 2 | 3 | This library relies upon rrdtool being installed an available. It can be used to create, destroy, update, fetch, dump, and restore an RRD database. It's a work in progress, and pull-requests are welcomed (with tests, please). 4 | 5 | ## Examples 6 | 7 | ### Creating a database 8 | 9 | RRD = require('rrd').RRD 10 | rrd = new RRD('valid.rrd') 11 | rrd.create ["DS:temperature:GAUGE:600:U:U", "RRA:AVERAGE:0.5:1:300"], {}, (err) -> 12 | if err 13 | console.log "error: #{err}" 14 | 15 | ### Updating a database 16 | 17 | RRD = require('rrd').RRD 18 | rrd = new RRD('valid.rrd') 19 | rrd.update new Date, [100], (err) -> 20 | if err 21 | console.log "error: #{err}" 22 | 23 | ### Querying the database 24 | 25 | rrdTime = (date) -> 26 | return Math.round(date.valueOf() / 1000) 27 | 28 | RRD = require('rrd').RRD 29 | rrd = new RRD('valid.rrd') 30 | rrd.fetch rrdTime(rrdStartTime), rrdTime(new Date), (err, results) -> 31 | if err 32 | console.log "error: #{err}" 33 | else 34 | console.log results 35 | 36 | 37 | This library relies upon rrdtool being installed an available. It can be used to create, destroy, update, fetch, dump, and restore an RRD database. It's a work in progress, and pull-requests are welcomed (with tests, please). 38 | 39 | 40 | ## Installation 41 | 42 | This library is available as a npm package. 43 | 44 | npm install rrd 45 | 46 | ## Projects 47 | 48 | This library is used by [Hot or Not](http://hot-or-not.plainlystated.com). 49 | 50 | Are you using this in an interesting project? I'd love to hear about it! 51 | 52 | ## More Info 53 | 54 | You can find more information on [my blog](http://www.plainlystated.com/tag/coffeescript-rrd). 55 | -------------------------------------------------------------------------------- /rrd.coffee: -------------------------------------------------------------------------------- 1 | sys = require('sys') 2 | exec = require('child_process').exec 3 | spawn = require('child_process').spawn 4 | fs = require('fs') 5 | 6 | RRDRecord = require('./rrdRecord').RRDRecord 7 | 8 | class RRD 9 | constructor: (@filename) -> 10 | 11 | create: (rrdArgs, options, cb) -> 12 | start = options.start ? new Date 13 | cmdArgs = ["create", @filename, "--start", _rrdTime(start), "--step", 300].concat rrdArgs 14 | 15 | console.log " - rrdtool #{cmdArgs.join(" ")}" 16 | 17 | proc = spawn("rrdtool", cmdArgs) 18 | err = "" 19 | proc.stderr.on 'data', (data) -> 20 | err += data 21 | proc.on 'exit', (code) -> 22 | if code == 0 23 | cb undefined, 'ok' 24 | else 25 | cb err, undefined 26 | 27 | destroy: (cb) -> 28 | fs.unlink(@filename, cb) 29 | 30 | dump: (cb) -> 31 | @rrdSpawn("dump", [], cb) 32 | 33 | rrdExec: (command, cmd_args, cb) -> 34 | cmd = "rrdtool #{command} #{@filename} #{cmd_args}" 35 | console.log cmd 36 | exec(cmd, {maxBuffer: 500 * 1024}, cb) 37 | 38 | rrdSpawn: (command, args, cb) -> 39 | proc = spawn("rrdtool", [command, @filename].concat(args)) 40 | err = "" 41 | out = "" 42 | proc.stderr.on 'data', (data) -> 43 | err += data 44 | proc.stdout.on 'data', (data) -> 45 | out += data 46 | proc.on 'exit', (code) -> 47 | if code == 0 48 | cb null, out 49 | else 50 | cb err, null 51 | 52 | update: (time, values, cb) -> 53 | @rrdSpawn("update", ["#{_rrdTime(time)}:#{values.join(':')}"], cb) 54 | 55 | fetch: (start, end, cb) -> 56 | this.rrdExec "fetch", "AVERAGE --start #{start} --end #{end}", (err, data) -> 57 | if err 58 | cb(err.message) 59 | return 60 | 61 | lines = data.split("\n") 62 | fieldNames = lines.shift().replace(new RegExp("^ +"), "").split(new RegExp(" +")) 63 | lines.shift() 64 | 65 | records = for line in lines 66 | continue if line == "" 67 | continue if line.match(" nan ") 68 | 69 | fields = line.split(new RegExp("[: ]+")) 70 | record = new RRDRecord(fields.shift(), fieldNames) 71 | for i in [0..fields.length-1] 72 | record[fieldNames[i]] = fields[i] 73 | record 74 | 75 | cb(undefined, records) 76 | 77 | _rrdTime = (date) -> 78 | return Math.round(date.valueOf() / 1000) 79 | 80 | RRD.restore = (filenameXML, filenameRRD, cb) -> 81 | exec "rrdtool restore #{filenameXML} #{filenameRRD}", cb 82 | exports.RRD = RRD 83 | -------------------------------------------------------------------------------- /rrd.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var RRD, RRDRecord, exec, fs, spawn, sys; 3 | sys = require('sys'); 4 | exec = require('child_process').exec; 5 | spawn = require('child_process').spawn; 6 | fs = require('fs'); 7 | RRDRecord = require('./rrdRecord').RRDRecord; 8 | RRD = (function() { 9 | var _rrdTime; 10 | function RRD(filename) { 11 | this.filename = filename; 12 | } 13 | RRD.prototype.create = function(rrdArgs, options, cb) { 14 | var cmdArgs, err, proc, start, _ref; 15 | start = (_ref = options.start) != null ? _ref : new Date; 16 | cmdArgs = ["create", this.filename, "--start", _rrdTime(start), "--step", 300].concat(rrdArgs); 17 | console.log(" - rrdtool " + (cmdArgs.join(" "))); 18 | proc = spawn("rrdtool", cmdArgs); 19 | err = ""; 20 | proc.stderr.on('data', function(data) { 21 | return err += data; 22 | }); 23 | return proc.on('exit', function(code) { 24 | if (code === 0) { 25 | return cb(void 0, 'ok'); 26 | } else { 27 | return cb(err, void 0); 28 | } 29 | }); 30 | }; 31 | RRD.prototype.destroy = function(cb) { 32 | return fs.unlink(this.filename, cb); 33 | }; 34 | RRD.prototype.dump = function(cb) { 35 | return this.rrdSpawn("dump", [], cb); 36 | }; 37 | RRD.prototype.rrdExec = function(command, cmd_args, cb) { 38 | var cmd; 39 | cmd = "rrdtool " + command + " " + this.filename + " " + cmd_args; 40 | console.log(cmd); 41 | return exec(cmd, { 42 | maxBuffer: 500 * 1024 43 | }, cb); 44 | }; 45 | RRD.prototype.rrdSpawn = function(command, args, cb) { 46 | var err, out, proc; 47 | proc = spawn("rrdtool", [command, this.filename].concat(args)); 48 | err = ""; 49 | out = ""; 50 | proc.stderr.on('data', function(data) { 51 | return err += data; 52 | }); 53 | proc.stdout.on('data', function(data) { 54 | return out += data; 55 | }); 56 | return proc.on('exit', function(code) { 57 | if (code === 0) { 58 | return cb(null, out); 59 | } else { 60 | return cb(err, null); 61 | } 62 | }); 63 | }; 64 | RRD.prototype.update = function(time, values, cb) { 65 | return this.rrdSpawn("update", ["" + (_rrdTime(time)) + ":" + (values.join(':'))], cb); 66 | }; 67 | RRD.prototype.fetch = function(start, end, cb) { 68 | return this.rrdExec("fetch", "AVERAGE --start " + start + " --end " + end, function(err, data) { 69 | var fieldNames, fields, i, line, lines, record, records; 70 | if (err) { 71 | cb(err.message); 72 | return; 73 | } 74 | lines = data.split("\n"); 75 | fieldNames = lines.shift().replace(new RegExp("^ +"), "").split(new RegExp(" +")); 76 | lines.shift(); 77 | records = (function() { 78 | var _i, _len, _ref, _results; 79 | _results = []; 80 | for (_i = 0, _len = lines.length; _i < _len; _i++) { 81 | line = lines[_i]; 82 | if (line === "") { 83 | continue; 84 | } 85 | if (line.match(" nan ")) { 86 | continue; 87 | } 88 | fields = line.split(new RegExp("[: ]+")); 89 | record = new RRDRecord(fields.shift(), fieldNames); 90 | for (i = 0, _ref = fields.length - 1; 0 <= _ref ? i <= _ref : i >= _ref; 0 <= _ref ? i++ : i--) { 91 | record[fieldNames[i]] = fields[i]; 92 | } 93 | _results.push(record); 94 | } 95 | return _results; 96 | })(); 97 | return cb(void 0, records); 98 | }); 99 | }; 100 | _rrdTime = function(date) { 101 | return Math.round(date.valueOf() / 1000); 102 | }; 103 | return RRD; 104 | })(); 105 | RRD.restore = function(filenameXML, filenameRRD, cb) { 106 | return exec("rrdtool restore " + filenameXML + " " + filenameRRD, cb); 107 | }; 108 | exports.RRD = RRD; 109 | }).call(this); 110 | -------------------------------------------------------------------------------- /test/rrd-test.coffee: -------------------------------------------------------------------------------- 1 | vows = require('vows') 2 | assert = require('assert') 3 | RRD = require('../rrd').RRD 4 | exec = require('child_process').exec 5 | 6 | vows.describe('RRD').addBatch( 7 | 'created a database with invalid contents': 8 | topic: (rrd) -> 9 | rrd = new RRD('empty-and-invalid.rrd') 10 | rrd.create([], {}, @callback) 11 | return 12 | 13 | 'gives an error': (err, result) -> 14 | assert.equal(err, 'ERROR: you must define at least one Round Robin Archive\n') 15 | assert.equal(result, undefined) 16 | 17 | 'creating a database with valid contents': 18 | topic: (rrd) -> 19 | rrd = new RRD('create-test.rrd') 20 | rrd.create(["DS:temperature:GAUGE:600:U:U", "RRA:AVERAGE:0.5:1:300"], {}, @callback) 21 | return 22 | 23 | 'gives no error': (err, result) -> 24 | assert.equal(err, undefined) 25 | assert.equal(result, 'ok') 26 | 27 | 'an invalid RRD': 28 | topic: new RRD('invalid.rrd') 29 | 30 | 'when fetching': 31 | topic: (rrd) -> 32 | rrd.fetch('1310664300', '1310664900', @callback) 33 | return 34 | 35 | 'returns an error': (err, result) -> 36 | assert.equal(err, "Command failed: ERROR: opening 'invalid.rrd': No such file or directory\n") 37 | 38 | 'does not return any results': (err, result) -> 39 | assert.equal(result, undefined) 40 | 41 | 'when dumping': 42 | topic: (rrd) -> 43 | rrd.dump(@callback) 44 | return 45 | 46 | 'returns an error': (err, xml) -> 47 | assert.equal(err, "ERROR: opening 'invalid.rrd': No such file or directory\n") 48 | 49 | 'returns no xml': (err, xml) -> 50 | assert.equal(xml, undefined) 51 | 52 | 'when updating': 53 | topic: (rrd) -> 54 | rrd.update(new Date, [1,2,3], @callback) 55 | return 56 | 57 | 'returns an error': (err) -> 58 | assert.equal(err, "ERROR: opening 'invalid.rrd': No such file or directory\n") 59 | 60 | 'a valid RRD': 61 | topic: new RRD('valid.rrd') 62 | 63 | 'when fetching': 64 | topic: (rrd) -> 65 | rrd.fetch('1310664300', '1310664900', @callback) 66 | return 67 | 68 | 'returns no error': (err, results) -> 69 | assert.equal(err, undefined) 70 | 71 | 'returns 3 results': (err, results) -> 72 | assert.equal(results.length, 3) 73 | 74 | 'has results with a timestamp': (err, results) -> 75 | assert.equal(results[0].timestamp, '1310664600') 76 | 77 | 'has results with appropriate fields': (err, results) -> 78 | assert.equal(results[0].temperature, '6.7620000000e+01') 79 | assert.equal(results[0].target_temp, '6.8000000000e+01') 80 | assert.equal(results[0].state, '0.0000000000e+00') 81 | 82 | 'when dumping': 83 | topic: (rrd) -> 84 | rrd.dump(@callback) 85 | return 86 | 87 | 'returns no error': (err, xml) -> 88 | assert.equal(err, null) 89 | 90 | 'returns some xml': (err, xml) -> 91 | assert.match(xml, /^<\?xml version="1.0" encoding="utf-8"\?>/) 92 | assert.match(xml, /<\/rrd>/) 93 | 94 | 'when updating': 95 | topic: (rrd) -> 96 | rrd.update(new Date, [1,2,3], @callback) 97 | return 98 | 99 | # Need to figure out how to test this without messing up subsequent test runs 100 | # 'returns no error': (err) -> 101 | # assert.equal(err, undefined) 102 | 'when restoring': 103 | 'a valid file': 104 | topic: () -> 105 | RRD.restore('valid.xml', "tmp-restore.rrd", @callback) 106 | return 107 | 108 | 'returns no error': (err) -> 109 | assert.equal(err, null) 110 | 111 | 'an invalid file': 112 | topic: () -> 113 | RRD.restore('invalid.xml', "tmp-restore.rrd", @callback) 114 | return 115 | 116 | # The underlying process never returns an error here.. need to figure out a better way to handle this 117 | # 'returns an error': (err) -> 118 | # assert.equal(err, 'some error') 119 | 120 | ).addBatch( 121 | 'cleanup': () -> 122 | exec('rm tmp-*') 123 | ).export(module, {error: false}) 124 | 125 | --------------------------------------------------------------------------------