UNPKG

metriks

Version:

A lightweight graphing framework for servers

473 lines (415 loc) 12.8 kB
var _ = require('underscore'); var async = require('async'); var exec = require('child_process').exec; var fs = require('fs'); var glob = require('glob'); var os = require('os'); var sys = require('sys'); var util = require('util'); var path = require('path'); var mkdirp = require('mkdirp'); var RRDTool = require('./rrdtool').RRDTool; var rrdtool = new RRDTool(); var unflatten = require('flat').unflatten; _.templateSettings = { interpolate: /\{(.+?)\}/g }; exports = module.exports = new Plugin(); exports.Plugin = Plugin; function Plugin(config) { this.defaultGraphOptions = { width : 1000, height : 600, watermark : 'kvz.io', font : 'DEFAULT:10:Inconsolata', tabwidth : 20, border : 2, zoom : 1, fullSizeMode : true, dynamicLabels: true, slopeMode : true, end : 'now', start : 'end-120000s', verticalLabel: '', lineTitles: [], lineColors: [ '#44B824FF', '#E992ECFF', '#FF0051FF', '#9AEBEAFF', '#D73A3FFF', '#44B824FF', '#6384B3FF', '#2F4D5DFF', '#E1E3EAFF', '#7F9B8BFF', '#FDFFA8FF', '#C7D5AEFF', '#8E7F57FF', '#FBFFF2FF', '#CF492CFF', '#9863B6FF', '#EF8E1CFF', '#45B175FF', '#3A96D0FF' ] }; this.defaultConfigOptions = { interval : '60', enabled : true, executable: true, }; // mgr config this.pluginFile = null; this.rrdDir = null; this.pngDir = null; this.autoWritePng = null; this.cli = null; _.extend(this, config); } Plugin.prototype.reload = function(cb) { var self = this; // Parse options from source's comments self.graph = {}; self.cli.info(util.format( 'Loading plugin %s. This also executes it with \'config\' parameter so you can print dynamic config. ', self.pluginFile )); var opts = { encoding : 'utf8', timeout : 10 * 1000, maxBuffer : 200 * 1024, killSignal: 'SIGTERM', cwd : path.dirname(self.pluginFile), env : process.ENV }; exec(self.pluginFile + ' config', opts, function (err, stdout, stderr) { if (err) { var msg = ''; msg += 'Cannot execute plugin ' + self.pluginFile + '. '; msg += 'If you want to disable please set \'# config.enable: false\'. '; msg += stderr; msg += err; msg += stdout; return cb(new Error(msg)); } // Parse comment header var flat = {}; var commentLines = stdout.match(/^#(.*)$/img); if (commentLines && commentLines.length) { commentLines.forEach(function(line) { var key = line.match(/^#\s*([^:]+)\s*/)[1]; var val = line.match(/:\s*(.*)\s*$/)[1]; flat[key] = val; }); } var nested = unflatten(flat); // Apply defaults to said config config = _.defaults(nested.config || {}, self.defaultConfigOptions); graph = _.defaults(nested.graph || {}, self.defaultGraphOptions); // Aply merged config to objects _.extend(self, config); _.extend(self.graph, graph); // Additional adding of default colors to any open slots, it's how you'd expect it to behave self.defaultGraphOptions.lineColors.forEach(function(color, colorIndex) { if (!(colorIndex in self.graph.lineColors)) { self.graph.lineColors[colorIndex] = self.defaultGraphOptions.lineColors[colorIndex]; } }); // Fixed plugin options self.name = path.basename(self.pluginFile, '.sh'); self.executable = !!(1 & parseInt ((fs.statSync(self.pluginFile).mode & parseInt('777', 8)).toString(8)[0])); // Smart plugin options if (!self.rrdFile) { self.rrdFile = util.format('%s/%s-%s.rrd', self.name, os.hostname(), self.name); } if (self.rrdFile.substr(0, 1) !== '/') { self.rrdFile = self.rrdDir + '/' + self.rrdFile; } if (!self.pngFile) { self.pngFile = util.format('%s/%s-%s.png', self.name, os.hostname(), self.name); } if (self.pngFile.substr(0, 1) !== '/') { self.pngFile = self.pngDir + '/' + self.pngFile; } if (self.enabled === 'false') { self.enabled = false; } else { self.enabled = true; } if (!self.timeout) { // Set plugin timeout to be slightly lower than interval if possible self.timeout = self.interval - 10; if (self.timeout < 10) { self.timeout = 50; } } self.interval = self.interval * 1; cb(null); }); }; /** * Loop a single plugin based on options.interval * @param {[type]} plugin */ Plugin.prototype.run = function(cb) { var self = this; var tasks = []; // Always write rrds tasks.push(function (callback) { self._writeRRD(function (err, output) { callback(err); }); }); // Optionally write pngs if (self.autoWritePng) { tasks.push(function (callback) { self._writePNG(function (err, output) { callback(err, output); }); }); } async.waterfall(tasks, function (err, output) { if (err) { return self.cli.error(util.format('failure %s.', err)); } self.cli.info(util.format( '%s task(s) for plugin %s complete', tasks.length, self.name )); cb(null); }); }; /** * Parse plugin output * @param {object} plugin * @param {string} output * @return {object} */ Plugin.prototype.parseSeries = function(stdout, stderr, cb) { var self = this; var series = []; var cnt = 0; stdout.trim().split('\n').forEach(function(line) { if (line.substr(0, 1) === '#') { return; } var columns = line.trim().split(/\s+/); var key; var value; cnt++; if (columns.length > 1) { // Name the line by first column key = columns.shift(); value = columns.join(' '); } else { // Name the line by row-index key = cnt; value = columns.join(' '); } // Sanitize and push series.push({ value: rrdtool.toVal(value), key : rrdtool.toDatasourceName(key), }); // console.log({ // line : line, // key : key, // value : value, // columns: columns, // cnt : cnt, // }); }); // If there is 1 row and no column name, name the line after the graph. // e.g.: 'uptime' if (series.length === 1 && rrdtool.isNumeric(series[0].key)) { if (!self.name) { return cb('Plugin has no name when it was needed to label simplistic series'); } series[0].key = rrdtool.toDatasourceName(self.name); } else { // console.log(series); } cb(null, series); }; Plugin.prototype._writeRRD = function(cb) { var self = this; async.waterfall([ function(callback) { // Mkdir var rrdDir = path.dirname(self.rrdFile); if (fs.existsSync(rrdDir)) { return callback(null); } self.cli.info(util.format('Creating directory %s', rrdDir)); mkdirp(rrdDir, function (err) { if (err) { return callback(err); } return callback(null); }); }, function(callback){ // Execute plugin var opts = { encoding : 'utf8', timeout : self.timeout * 1000, maxBuffer : 200 * 1024, killSignal: 'SIGTERM', cwd : path.dirname(self.pluginFile), env : process.env }; exec(self.pluginFile, opts, function (err, stdout, stderr) { if (err) { return callback('Cannot execute self. ' + stderr); } if (stderr) { self.cli.error(util.format('Saw stderr while running plugin: ' + stderr)); } callback(err, stdout, stderr); }); }, function(stdout, stderr, callback){ // Convert output to series self.parseSeries(stdout, stderr, function (err, series) { if (err) { return callback(err); } callback(null, series); }); }, function(series, callback){ // Find info rrdtool.info(self.rrdFile, [], function(err, info) { if (err) { return callback(err); } callback(null, series, info); }); }, function(series, info, callback){ // Create rrds if needed var values = []; var options = []; series.forEach(function (item) { options.push(_.template('DS:{key}:GAUGE:600:U:U')({ key: item.key })); values.push(item.value); }); options.push(_.template('RRA:AVERAGE:0.5:1:300')({})); if (info === null) { // Info is null if the rrd didn't exist yet rrdtool.create(self.rrdFile, options, function(err, output) { return callback(err, values); }); } else { var datasourcesInRRD = _.keys(info.ds); series.forEach(function(item, i) { if (datasourcesInRRD[i] !== item.key) { return callback(util.format( 'Plugin %s generates datasource "%s", but rrd %s holds "%s" in this location (%s). All keys: %s', self.pluginFile, item.key, self.rrdFile, datasourcesInRRD[item.key], i, datasourcesInRRD.join(', ') )); } }); if (series.length !== datasourcesInRRD.length) { return callback(util.format( 'Plugin %s generates %s datasources, but rrd %s was created with %s. ', self.pluginFile, series.length, self.rrdFile, datasourcesInRRD.length )); } return callback(null, values); } }, function(values, callback){ // Update rrd with series rrdtool.update(self.rrdFile, new Date(), values, [], function(err, output) { callback(null); }); } ], function (err) { if (err) { throw err; } cb(err); }); }; Plugin.prototype._writePNG = function (cb) { var self = this; async.waterfall([ function(callback) { // Mkdir var pngDir = path.dirname(self.pngFile); if (fs.existsSync(pngDir)) { return callback(null); } self.cli.info(util.format('Creating directory %s', pngDir)); mkdirp(pngDir, function (err) { if (err) { return callback(err); } return callback(null); }); }, function (callback) { rrdtool.info(self.rrdFile, [], function(err, info) { if (err) { return callback(err); } // Copy plugin options var graphParams = {}; _.extend(graphParams, _.omit(self.graph, 'lineColors', 'lineTitles')); var options = []; options.push(graphParams); options.push('--color'); options.push(util.format('BACK%s', '#1D1D1DFF')); options.push('--color'); options.push(util.format('CANVAS%s', '#141414FF')); options.push('--color'); options.push(util.format('SHADEA%s', '#282A2AFF')); options.push('--color'); options.push(util.format('SHADEB%s', '#121212FF')); options.push('--color'); options.push(util.format('GRID%s', '#232323FF')); options.push('--color'); options.push(util.format('MGRID%s', '#232323EE')); options.push('--color'); options.push(util.format('FRAME%s', '#2A1F1CFF')); options.push('--color'); options.push(util.format('FONT%s', '#FFFFFFEE')); // options.push(util.format('FONT%s', '#2A1F1CFF')); options.push('--color'); options.push(util.format('AXIS%s', '#4A4A4AFF')); var useLinecolors = []; var dss = _.keys(info.ds); dss.forEach(function (key, index) { if (useLinecolors.length === 0) { // We need a color refil useLinecolors = useLinecolors.concat(self.graph.lineColors); } var lineTitle = self.graph.lineTitles[index] ? self.graph.lineTitles[index] : key; var dsParams = { col : useLinecolors.shift(), descr : rrdtool.toTitle(lineTitle), ds : key, id : key + 'a', rrdFile: self.rrdFile, }; options.push(_.template('DEF:{ id }={ rrdFile }:{ ds }:AVERAGE')(dsParams)); options.push(_.template('LINE1:{ id }{ col }:"{ descr }\\l"')(dsParams)); }); callback(null, options); }); }, function(options, callback) { rrdtool.graph(self.pngFile, options, function(err, output) { if (err) { return callback(err); } callback(null); }); } ], function(err) { cb(err); }); };