'use strict'; // netdata // real-time performance and health monitoring, done right! // (C) 2016 Costa Tsaousis // GPL v3+ var url = require('url'); var http = require('http'); var util = require('util'); /* var netdata = require('netdata'); var example_chart = { id: 'id', // the unique id of the chart name: 'name', // the name of the chart title: 'title', // the title of the chart units: 'units', // the units of the chart dimensions family: 'family', // the family of the chart context: 'context', // the context of the chart type: netdata.chartTypes.line, // the type of the chart priority: 0, // the priority relative to others in the same family update_every: 1, // the expected update frequency of the chart dimensions: { 'dim1': { id: 'dim1', // the unique id of the dimension name: 'name', // the name of the dimension algorithm: netdata.chartAlgorithms.absolute, // the id of the netdata algorithm multiplier: 1, // the multiplier divisor: 1, // the divisor hidden: false, // is hidden (boolean) }, 'dim2': { id: 'dim2', // the unique id of the dimension name: 'name', // the name of the dimension algorithm: 'absolute', // the id of the netdata algorithm multiplier: 1, // the multiplier divisor: 1, // the divisor hidden: false, // is hidden (boolean) } // add as many dimensions as needed } }; */ var netdata = { options: { filename: __filename, DEBUG: false, update_every: 1 }, chartAlgorithms: { incremental: 'incremental', absolute: 'absolute', percentage_of_absolute_row: 'percentage-of-absolute-row', percentage_of_incremental_row: 'percentage-of-incremental-row' }, chartTypes: { line: 'line', area: 'area', stacked: 'stacked' }, services: new Array(), modules_configuring: 0, charts: {}, processors: { http: { name: 'http', process: function(service, callback) { var __DEBUG = netdata.options.DEBUG; if(__DEBUG === true) netdata.debug(service.module.name + ': ' + service.name + ': making ' + this.name + ' request: ' + netdata.stringify(service.request)); var req = http.request(service.request, function(response) { if(__DEBUG === true) netdata.debug(service.module.name + ': ' + service.name + ': got server response...'); var end = false; var data = ''; response.setEncoding('utf8'); if(response.statusCode !== 200) { if(end === false) { service.error('Got HTTP code ' + response.statusCode + ', failed to get data.'); end = true; return callback(null); } } response.on('data', function(chunk) { if(end === false) data += chunk; }); response.on('error', function() { if(end === false) { service.error(': Read error, failed to get data.'); end = true; return callback(null); } }); response.on('end', function() { if(end === false) { if(__DEBUG === true) netdata.debug(service.module.name + ': ' + service.name + ': read completed.'); end = true; return callback(data); } }); }); req.on('error', function(e) { if(__DEBUG === true) netdata.debug('Failed to make request: ' + netdata.stringify(service.request) + ', message: ' + e.message); service.error('Failed to make request, message: ' + e.message); return callback(null); }); // write data to request body if(typeof service.postData !== 'undefined' && service.request.method === 'POST') { if(__DEBUG === true) netdata.debug(service.module.name + ': ' + service.name + ': posting data: ' + service.postData); req.write(service.postData); } req.end(); } } }, stringify: function(obj) { return util.inspect(obj, {depth: 10}); }, zeropad2: function(s) { return ("00" + s).slice(-2); }, logdate: function(d) { if(typeof d === 'undefined') d = new Date(); return d.getFullYear().toString() + '-' + this.zeropad2(d.getMonth() + 1) + '-' + this.zeropad2(d.getDate()) + ' ' + this.zeropad2(d.getHours()) + ':' + this.zeropad2(d.getMinutes()) + ':' + this.zeropad2(d.getSeconds()); }, // show debug info, if debug is enabled debug: function(msg) { if(this.options.DEBUG === true) { console.error(this.logdate() + ': ' + netdata.options.filename + ': DEBUG: ' + ((typeof(msg) === 'object')?netdata.stringify(msg):msg).toString()); } }, // log an error error: function(msg) { console.error(this.logdate() + ': ' + netdata.options.filename + ': ERROR: ' + ((typeof(msg) === 'object')?netdata.stringify(msg):msg).toString()); }, // send data to netdata send: function(msg) { console.log(msg.toString()); }, service: function(service) { if(typeof service === 'undefined') service = {}; var now = Date.now(); service._current_chart = null; // the current chart we work on service._queue = ''; // data to be sent to netdata service.error_reported = false; // error log flood control service.added = false; // added to netdata.services service.enabled = true; service.updates = 0; service.running = false; service.started = 0; service.ended = 0; if(typeof service.module === 'undefined') { service.module = { name: 'not-defined-module' }; service.error('Attempted to create service without a module.'); service.enabled = false; } if(typeof service.name === 'undefined') { service.name = 'unnamed@' + service.module.name + '/' + now; } if(typeof service.processor === 'undefined') service.processor = netdata.processors.http; if(typeof service.update_every === 'undefined') service.update_every = service.module.update_every; if(typeof service.update_every === 'undefined') service.update_every = netdata.options.update_every; if(service.update_every < netdata.options.update_every) service.update_every = netdata.options.update_every; // align the runs service.next_run = now - (now % (service.update_every * 1000)) + (service.update_every * 1000); service.commit = function() { if(this.added !== true) { this.added = true; var now = Date.now(); this.next_run = now - (now % (service.update_every * 1000)) + (service.update_every * 1000); netdata.services.push(this); if(netdata.options.DEBUG === true) netdata.debug(this.module.name + ': ' + this.name + ': service committed.'); } }; service.execute = function(responseProcessor) { var __DEBUG = netdata.options.DEBUG; if(service.enabled === false) return responseProcessor(null); this.module.active++; this.running = true; this.started = Date.now(); this.updates++; if(__DEBUG === true) netdata.debug(this.module.name + ': ' + this.name + ': making ' + this.processor.name + ' request: ' + netdata.stringify(this)); this.processor.process(this, function(response) { service.ended = Date.now(); service.duration = service.ended - service.started; if(typeof response === 'undefined') response = null; if(response !== null) service.errorClear(); if(__DEBUG === true) netdata.debug(service.module.name + ': ' + service.name + ': processing ' + service.processor.name + ' response (received in ' + (service.ended - service.started).toString() + ' ms)'); responseProcessor(service, response); service.running = false; service.module.active--; if(service.module.active < 0) { service.module.active = 0; if(__DEBUG === true) netdata.debug(service.module.name + ': active module counter below zero.'); } if(service.module.active === 0) { // check if we run under configure if(service.module.configure_callback !== null) { if(__DEBUG === true) netdata.debug(service.module.name + ': configuration finish callback called from processResponse().'); var configure_callback = service.module.configure_callback; service.module.configure_callback = null; configure_callback(); } } }); }; service.update = function() { if(netdata.options.DEBUG === true) netdata.debug(this.module.name + ': ' + this.name + ': starting data collection...'); this.module.update(this, function() { if(netdata.options.DEBUG === true) netdata.debug(service.module.name + ': ' + service.name + ': data collection ended in ' + service.duration.toString() + ' ms.'); }); }; service.error = function(message) { if(this.error_reported === false) { netdata.error(this.module.name + ': ' + this.name + ': ' + message); this.error_reported = true; } else if(netdata.options.DEBUG === true) netdata.debug(this.module.name + ': ' + this.name + ': ' + message); }; service.errorClear = function() { this.error_reported = false; }; service.queue = function(txt) { this._queue += txt + '\n'; }; service._send_chart_to_netdata = function(chart) { // internal function to send a chart to netdata this.queue('CHART "' + chart.id + '" "' + chart.name + '" "' + chart.title + '" "' + chart.units + '" "' + chart.family + '" "' + chart.context + '" "' + chart.type + '" ' + chart.priority.toString() + ' ' + chart.update_every.toString()); if(typeof(chart.dimensions) !== 'undefined') { var dims = Object.keys(chart.dimensions); var len = dims.length; while(len--) { var d = chart.dimensions[dims[len]]; this.queue('DIMENSION "' + d.id + '" "' + d.name + '" "' + d.algorithm + '" ' + d.multiplier.toString() + ' ' + d.divisor.toString() + ' ' + ((d.hidden === true) ? 'hidden' : '').toString()); d._created = true; d._updated = false; } } chart._created = true; chart._updated = false; }; // begin data collection for a chart service.begin = function(chart) { if(this._current_chart !== null && this._current_chart !== chart) { this.error('Called begin() for chart ' + chart.id + ' while chart ' + this._current_chart.id + ' is still open. Closing it.'); this.end(); } if(typeof(chart.id) === 'undefined' || netdata.charts[chart.id] !== chart) { this.error('Called begin() for chart ' + chart.id + ' that is not mine. Where did you find it? Ignoring it.'); return false; } if(netdata.options.DEBUG === true) netdata.debug('setting current chart to ' + chart.id); this._current_chart = chart; this._current_chart._began = true; if(this._current_chart._dimensions_count !== 0) { if(this._current_chart._created === false || this._current_chart._updated === true) this._send_chart_to_netdata(this._current_chart); var now = this.ended; this.queue('BEGIN ' + this._current_chart.id + ' ' + ((this._current_chart._last_updated > 0)?((now - this._current_chart._last_updated) * 1000):'').toString()); } // else this.error('Called begin() for chart ' + chart.id + ' which is empty.'); this._current_chart._last_updated = now; this._current_chart._began = true; this._current_chart._counter++; return true; }; // set a collected value for a chart // we do most things on the first value we attempt to set service.set = function(dimension, value) { if(this._current_chart === null) { this.error('Called set(' + dimension + ', ' + value + ') without an open chart.'); return false; } if(typeof(this._current_chart.dimensions[dimension]) === 'undefined') { this.error('Called set(' + dimension + ', ' + value + ') but dimension "' + dimension + '" does not exist in chart "' + this._current_chart.id + '".'); return false; } if(typeof value === 'undefined' || value === null) return false; if(this._current_chart._dimensions_count !== 0) { if (value instanceof Buffer) this.queue('SET ' + dimension + ' = 0x' + value.toString('hex')); else this.queue('SET ' + dimension + ' = ' + value.toString()); } return true; }; // end data collection for the current chart - after calling begin() service.end = function() { if(this._current_chart !== null && this._current_chart._began === false) { this.error('Called end() without an open chart.'); return false; } if(this._current_chart._dimensions_count !== 0) { this.queue('END'); netdata.send(this._queue); } this._queue = ''; this._current_chart._began = false; if(netdata.options.DEBUG === true) netdata.debug('sent chart ' + this._current_chart.id); this._current_chart = null; return true; }; // discard the collected values for the current chart - after calling begin() service.flush = function() { if(this._current_chart === null || this._current_chart._began === false) { this.error('Called flush() without an open chart.'); return false; } this._queue = ''; this._current_chart._began = false; this._current_chart = null; return true; }; // create a netdata chart service.chart = function(id, chart) { var __DEBUG = netdata.options.DEBUG; if(typeof(netdata.charts[id]) === 'undefined') { netdata.charts[id] = { _created: false, _updated: true, _began: false, _counter: 0, _last_updated: 0, _dimensions_count: 0, id: id, name: id, title: 'untitled chart', units: 'a unit', family: '', context: '', type: netdata.chartTypes.line, priority: 50000, update_every: netdata.options.update_every, dimensions: {} }; } var c = netdata.charts[id]; if(typeof(chart.name) !== 'undefined' && chart.name !== c.name) { if(__DEBUG === true) netdata.debug('chart ' + id + ' updated its name'); c.name = chart.name; c._updated = true; } if(typeof(chart.title) !== 'undefined' && chart.title !== c.title) { if(__DEBUG === true) netdata.debug('chart ' + id + ' updated its title'); c.title = chart.title; c._updated = true; } if(typeof(chart.units) !== 'undefined' && chart.units !== c.units) { if(__DEBUG === true) netdata.debug('chart ' + id + ' updated its units'); c.units = chart.units; c._updated = true; } if(typeof(chart.family) !== 'undefined' && chart.family !== c.family) { if(__DEBUG === true) netdata.debug('chart ' + id + ' updated its family'); c.family = chart.family; c._updated = true; } if(typeof(chart.context) !== 'undefined' && chart.context !== c.context) { if(__DEBUG === true) netdata.debug('chart ' + id + ' updated its context'); c.context = chart.context; c._updated = true; } if(typeof(chart.type) !== 'undefined' && chart.type !== c.type) { if(__DEBUG === true) netdata.debug('chart ' + id + ' updated its type'); c.type = chart.type; c._updated = true; } if(typeof(chart.priority) !== 'undefined' && chart.priority !== c.priority) { if(__DEBUG === true) netdata.debug('chart ' + id + ' updated its priority'); c.priority = chart.priority; c._updated = true; } if(typeof(chart.update_every) !== 'undefined' && chart.update_every !== c.update_every) { if(__DEBUG === true) netdata.debug('chart ' + id + ' updated its update_every from ' + c.update_every + ' to ' + chart.update_every); c.update_every = chart.update_every; c._updated = true; } if(typeof(chart.dimensions) !== 'undefined') { var dims = Object.keys(chart.dimensions); var len = dims.length; while(len--) { var x = dims[len]; if(typeof(c.dimensions[x]) === 'undefined') { c._dimensions_count++; c.dimensions[x] = { _created: false, _updated: false, id: x, // the unique id of the dimension name: x, // the name of the dimension algorithm: netdata.chartAlgorithms.absolute, // the id of the netdata algorithm multiplier: 1, // the multiplier divisor: 1, // the divisor hidden: false // is hidden (boolean) }; if(__DEBUG === true) netdata.debug('chart ' + id + ' created dimension ' + x); c._updated = true; } var dim = chart.dimensions[x]; var d = c.dimensions[x]; if(typeof(dim.name) !== 'undefined' && d.name !== dim.name) { if(__DEBUG === true) netdata.debug('chart ' + id + ', dimension ' + x + ' updated its name'); d.name = dim.name; d._updated = true; } if(typeof(dim.algorithm) !== 'undefined' && d.algorithm !== dim.algorithm) { if(__DEBUG === true) netdata.debug('chart ' + id + ', dimension ' + x + ' updated its algorithm from ' + d.algorithm + ' to ' + dim.algorithm); d.algorithm = dim.algorithm; d._updated = true; } if(typeof(dim.multiplier) !== 'undefined' && d.multiplier !== dim.multiplier) { if(__DEBUG === true) netdata.debug('chart ' + id + ', dimension ' + x + ' updated its multiplier'); d.multiplier = dim.multiplier; d._updated = true; } if(typeof(dim.divisor) !== 'undefined' && d.divisor !== dim.divisor) { if(__DEBUG === true) netdata.debug('chart ' + id + ', dimension ' + x + ' updated its divisor'); d.divisor = dim.divisor; d._updated = true; } if(typeof(dim.hidden) !== 'undefined' && d.hidden !== dim.hidden) { if(__DEBUG === true) netdata.debug('chart ' + id + ', dimension ' + x + ' updated its hidden status'); d.hidden = dim.hidden; d._updated = true; } if(d._updated) c._updated = true; } } //if(netdata.options.DEBUG === true) netdata.debug(netdata.charts); return netdata.charts[id]; }; return service; }, runAllServices: function() { if(netdata.options.DEBUG === true) netdata.debug('runAllServices()'); var now = Date.now(); var len = netdata.services.length; while(len--) { var service = netdata.services[len]; if(service.enabled === false || service.running === true) continue; if(now <= service.next_run) continue; service.update(); now = Date.now(); service.next_run = now - (now % (service.update_every * 1000)) + (service.update_every * 1000); } // 1/10th of update_every in pause setTimeout(netdata.runAllServices, netdata.options.update_every * 100); }, start: function() { if(netdata.options.DEBUG === true) this.debug('started, services: ' + netdata.stringify(this.services)); if(this.services.length === 0) { this.disableNodePlugin(); // eslint suggested way to exit var exit = process.exit; exit(1); } else this.runAllServices(); }, // disable the whole node.js plugin disableNodePlugin: function() { this.send('DISABLE'); // eslint suggested way to exit var exit = process.exit; exit(1); }, requestFromParams: function(protocol, hostname, port, path, method) { return { protocol: protocol, hostname: hostname, port: port, path: path, //family: 4, method: method, headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Connection': 'keep-alive' }, agent: new http.Agent({ keepAlive: true, keepAliveMsecs: netdata.options.update_every * 1000, maxSockets: 2, // it must be 2 to work maxFreeSockets: 1 }) }; }, requestFromURL: function(a_url) { var u = url.parse(a_url); return netdata.requestFromParams(u.protocol, u.hostname, u.port, u.path, 'GET'); }, configure: function(module, config, callback) { if(netdata.options.DEBUG === true) this.debug(module.name + ': configuring (update_every: ' + this.options.update_every + ')...'); module.active = 0; module.update_every = this.options.update_every; if(typeof config.update_every !== 'undefined') module.update_every = config.update_every; module.enable_autodetect = (config.enable_autodetect)?true:false; if(typeof(callback) === 'function') module.configure_callback = callback; else module.configure_callback = null; var added = module.configure(config); if(netdata.options.DEBUG === true) this.debug(module.name + ': configured, reporting ' + added + ' eligible services.'); if(module.configure_callback !== null && added === 0) { if(netdata.options.DEBUG === true) this.debug(module.name + ': configuration finish callback called from configure().'); var configure_callback = module.configure_callback; module.configure_callback = null; configure_callback(); } return added; } }; if(netdata.options.DEBUG === true) netdata.debug('loaded netdata from:', __filename); module.exports = netdata;