netdata.js 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653
  1. 'use strict';
  2. // netdata
  3. // real-time performance and health monitoring, done right!
  4. // (C) 2016 Costa Tsaousis <costa@tsaousis.gr>
  5. // GPL v3+
  6. var url = require('url');
  7. var http = require('http');
  8. var util = require('util');
  9. /*
  10. var netdata = require('netdata');
  11. var example_chart = {
  12. id: 'id', // the unique id of the chart
  13. name: 'name', // the name of the chart
  14. title: 'title', // the title of the chart
  15. units: 'units', // the units of the chart dimensions
  16. family: 'family', // the family of the chart
  17. context: 'context', // the context of the chart
  18. type: netdata.chartTypes.line, // the type of the chart
  19. priority: 0, // the priority relative to others in the same family
  20. update_every: 1, // the expected update frequency of the chart
  21. dimensions: {
  22. 'dim1': {
  23. id: 'dim1', // the unique id of the dimension
  24. name: 'name', // the name of the dimension
  25. algorithm: netdata.chartAlgorithms.absolute, // the id of the netdata algorithm
  26. multiplier: 1, // the multiplier
  27. divisor: 1, // the divisor
  28. hidden: false, // is hidden (boolean)
  29. },
  30. 'dim2': {
  31. id: 'dim2', // the unique id of the dimension
  32. name: 'name', // the name of the dimension
  33. algorithm: 'absolute', // the id of the netdata algorithm
  34. multiplier: 1, // the multiplier
  35. divisor: 1, // the divisor
  36. hidden: false, // is hidden (boolean)
  37. }
  38. // add as many dimensions as needed
  39. }
  40. };
  41. */
  42. var netdata = {
  43. options: {
  44. filename: __filename,
  45. DEBUG: false,
  46. update_every: 1
  47. },
  48. chartAlgorithms: {
  49. incremental: 'incremental',
  50. absolute: 'absolute',
  51. percentage_of_absolute_row: 'percentage-of-absolute-row',
  52. percentage_of_incremental_row: 'percentage-of-incremental-row'
  53. },
  54. chartTypes: {
  55. line: 'line',
  56. area: 'area',
  57. stacked: 'stacked'
  58. },
  59. services: new Array(),
  60. modules_configuring: 0,
  61. charts: {},
  62. processors: {
  63. http: {
  64. name: 'http',
  65. process: function(service, callback) {
  66. var __DEBUG = netdata.options.DEBUG;
  67. if(__DEBUG === true)
  68. netdata.debug(service.module.name + ': ' + service.name + ': making ' + this.name + ' request: ' + netdata.stringify(service.request));
  69. var req = http.request(service.request, function(response) {
  70. if(__DEBUG === true) netdata.debug(service.module.name + ': ' + service.name + ': got server response...');
  71. var end = false;
  72. var data = '';
  73. response.setEncoding('utf8');
  74. if(response.statusCode !== 200) {
  75. if(end === false) {
  76. service.error('Got HTTP code ' + response.statusCode + ', failed to get data.');
  77. end = true;
  78. return callback(null);
  79. }
  80. }
  81. response.on('data', function(chunk) {
  82. if(end === false) data += chunk;
  83. });
  84. response.on('error', function() {
  85. if(end === false) {
  86. service.error(': Read error, failed to get data.');
  87. end = true;
  88. return callback(null);
  89. }
  90. });
  91. response.on('end', function() {
  92. if(end === false) {
  93. if(__DEBUG === true) netdata.debug(service.module.name + ': ' + service.name + ': read completed.');
  94. end = true;
  95. return callback(data);
  96. }
  97. });
  98. });
  99. req.on('error', function(e) {
  100. if(__DEBUG === true) netdata.debug('Failed to make request: ' + netdata.stringify(service.request) + ', message: ' + e.message);
  101. service.error('Failed to make request, message: ' + e.message);
  102. return callback(null);
  103. });
  104. // write data to request body
  105. if(typeof service.postData !== 'undefined' && service.request.method === 'POST') {
  106. if(__DEBUG === true) netdata.debug(service.module.name + ': ' + service.name + ': posting data: ' + service.postData);
  107. req.write(service.postData);
  108. }
  109. req.end();
  110. }
  111. }
  112. },
  113. stringify: function(obj) {
  114. return util.inspect(obj, {depth: 10});
  115. },
  116. zeropad2: function(s) {
  117. return ("00" + s).slice(-2);
  118. },
  119. logdate: function(d) {
  120. if(typeof d === 'undefined') d = new Date();
  121. return d.getFullYear().toString() + '-' + this.zeropad2(d.getMonth() + 1) + '-' + this.zeropad2(d.getDate())
  122. + ' ' + this.zeropad2(d.getHours()) + ':' + this.zeropad2(d.getMinutes()) + ':' + this.zeropad2(d.getSeconds());
  123. },
  124. // show debug info, if debug is enabled
  125. debug: function(msg) {
  126. if(this.options.DEBUG === true) {
  127. console.error(this.logdate() + ': ' + netdata.options.filename + ': DEBUG: ' + ((typeof(msg) === 'object')?netdata.stringify(msg):msg).toString());
  128. }
  129. },
  130. // log an error
  131. error: function(msg) {
  132. console.error(this.logdate() + ': ' + netdata.options.filename + ': ERROR: ' + ((typeof(msg) === 'object')?netdata.stringify(msg):msg).toString());
  133. },
  134. // send data to netdata
  135. send: function(msg) {
  136. console.log(msg.toString());
  137. },
  138. service: function(service) {
  139. if(typeof service === 'undefined')
  140. service = {};
  141. var now = Date.now();
  142. service._current_chart = null; // the current chart we work on
  143. service._queue = ''; // data to be sent to netdata
  144. service.error_reported = false; // error log flood control
  145. service.added = false; // added to netdata.services
  146. service.enabled = true;
  147. service.updates = 0;
  148. service.running = false;
  149. service.started = 0;
  150. service.ended = 0;
  151. if(typeof service.module === 'undefined') {
  152. service.module = { name: 'not-defined-module' };
  153. service.error('Attempted to create service without a module.');
  154. service.enabled = false;
  155. }
  156. if(typeof service.name === 'undefined') {
  157. service.name = 'unnamed@' + service.module.name + '/' + now;
  158. }
  159. if(typeof service.processor === 'undefined')
  160. service.processor = netdata.processors.http;
  161. if(typeof service.update_every === 'undefined')
  162. service.update_every = service.module.update_every;
  163. if(typeof service.update_every === 'undefined')
  164. service.update_every = netdata.options.update_every;
  165. if(service.update_every < netdata.options.update_every)
  166. service.update_every = netdata.options.update_every;
  167. // align the runs
  168. service.next_run = now - (now % (service.update_every * 1000)) + (service.update_every * 1000);
  169. service.commit = function() {
  170. if(this.added !== true) {
  171. this.added = true;
  172. var now = Date.now();
  173. this.next_run = now - (now % (service.update_every * 1000)) + (service.update_every * 1000);
  174. netdata.services.push(this);
  175. if(netdata.options.DEBUG === true) netdata.debug(this.module.name + ': ' + this.name + ': service committed.');
  176. }
  177. };
  178. service.execute = function(responseProcessor) {
  179. var __DEBUG = netdata.options.DEBUG;
  180. if(service.enabled === false)
  181. return responseProcessor(null);
  182. this.module.active++;
  183. this.running = true;
  184. this.started = Date.now();
  185. this.updates++;
  186. if(__DEBUG === true)
  187. netdata.debug(this.module.name + ': ' + this.name + ': making ' + this.processor.name + ' request: ' + netdata.stringify(this));
  188. this.processor.process(this, function(response) {
  189. service.ended = Date.now();
  190. service.duration = service.ended - service.started;
  191. if(typeof response === 'undefined')
  192. response = null;
  193. if(response !== null)
  194. service.errorClear();
  195. if(__DEBUG === true)
  196. netdata.debug(service.module.name + ': ' + service.name + ': processing ' + service.processor.name + ' response (received in ' + (service.ended - service.started).toString() + ' ms)');
  197. responseProcessor(service, response);
  198. service.running = false;
  199. service.module.active--;
  200. if(service.module.active < 0) {
  201. service.module.active = 0;
  202. if(__DEBUG === true)
  203. netdata.debug(service.module.name + ': active module counter below zero.');
  204. }
  205. if(service.module.active === 0) {
  206. // check if we run under configure
  207. if(service.module.configure_callback !== null) {
  208. if(__DEBUG === true)
  209. netdata.debug(service.module.name + ': configuration finish callback called from processResponse().');
  210. var configure_callback = service.module.configure_callback;
  211. service.module.configure_callback = null;
  212. configure_callback();
  213. }
  214. }
  215. });
  216. };
  217. service.update = function() {
  218. if(netdata.options.DEBUG === true)
  219. netdata.debug(this.module.name + ': ' + this.name + ': starting data collection...');
  220. this.module.update(this, function() {
  221. if(netdata.options.DEBUG === true)
  222. netdata.debug(service.module.name + ': ' + service.name + ': data collection ended in ' + service.duration.toString() + ' ms.');
  223. });
  224. };
  225. service.error = function(message) {
  226. if(this.error_reported === false) {
  227. netdata.error(this.module.name + ': ' + this.name + ': ' + message);
  228. this.error_reported = true;
  229. }
  230. else if(netdata.options.DEBUG === true)
  231. netdata.debug(this.module.name + ': ' + this.name + ': ' + message);
  232. };
  233. service.errorClear = function() {
  234. this.error_reported = false;
  235. };
  236. service.queue = function(txt) {
  237. this._queue += txt + '\n';
  238. };
  239. service._send_chart_to_netdata = function(chart) {
  240. // internal function to send a chart to netdata
  241. this.queue('CHART "' + chart.id + '" "' + chart.name + '" "' + chart.title + '" "' + chart.units + '" "' + chart.family + '" "' + chart.context + '" "' + chart.type + '" ' + chart.priority.toString() + ' ' + chart.update_every.toString());
  242. if(typeof(chart.dimensions) !== 'undefined') {
  243. var dims = Object.keys(chart.dimensions);
  244. var len = dims.length;
  245. while(len--) {
  246. var d = chart.dimensions[dims[len]];
  247. this.queue('DIMENSION "' + d.id + '" "' + d.name + '" "' + d.algorithm + '" ' + d.multiplier.toString() + ' ' + d.divisor.toString() + ' ' + ((d.hidden === true) ? 'hidden' : '').toString());
  248. d._created = true;
  249. d._updated = false;
  250. }
  251. }
  252. chart._created = true;
  253. chart._updated = false;
  254. };
  255. // begin data collection for a chart
  256. service.begin = function(chart) {
  257. if(this._current_chart !== null && this._current_chart !== chart) {
  258. this.error('Called begin() for chart ' + chart.id + ' while chart ' + this._current_chart.id + ' is still open. Closing it.');
  259. this.end();
  260. }
  261. if(typeof(chart.id) === 'undefined' || netdata.charts[chart.id] !== chart) {
  262. this.error('Called begin() for chart ' + chart.id + ' that is not mine. Where did you find it? Ignoring it.');
  263. return false;
  264. }
  265. if(netdata.options.DEBUG === true) netdata.debug('setting current chart to ' + chart.id);
  266. this._current_chart = chart;
  267. this._current_chart._began = true;
  268. if(this._current_chart._dimensions_count !== 0) {
  269. if(this._current_chart._created === false || this._current_chart._updated === true)
  270. this._send_chart_to_netdata(this._current_chart);
  271. var now = this.ended;
  272. this.queue('BEGIN ' + this._current_chart.id + ' ' + ((this._current_chart._last_updated > 0)?((now - this._current_chart._last_updated) * 1000):'').toString());
  273. }
  274. // else this.error('Called begin() for chart ' + chart.id + ' which is empty.');
  275. this._current_chart._last_updated = now;
  276. this._current_chart._began = true;
  277. this._current_chart._counter++;
  278. return true;
  279. };
  280. // set a collected value for a chart
  281. // we do most things on the first value we attempt to set
  282. service.set = function(dimension, value) {
  283. if(this._current_chart === null) {
  284. this.error('Called set(' + dimension + ', ' + value + ') without an open chart.');
  285. return false;
  286. }
  287. if(typeof(this._current_chart.dimensions[dimension]) === 'undefined') {
  288. this.error('Called set(' + dimension + ', ' + value + ') but dimension "' + dimension + '" does not exist in chart "' + this._current_chart.id + '".');
  289. return false;
  290. }
  291. if(typeof value === 'undefined' || value === null)
  292. return false;
  293. if(this._current_chart._dimensions_count !== 0) {
  294. if (value instanceof Buffer)
  295. this.queue('SET ' + dimension + ' = 0x' + value.toString('hex'));
  296. else
  297. this.queue('SET ' + dimension + ' = ' + value.toString());
  298. }
  299. return true;
  300. };
  301. // end data collection for the current chart - after calling begin()
  302. service.end = function() {
  303. if(this._current_chart !== null && this._current_chart._began === false) {
  304. this.error('Called end() without an open chart.');
  305. return false;
  306. }
  307. if(this._current_chart._dimensions_count !== 0) {
  308. this.queue('END');
  309. netdata.send(this._queue);
  310. }
  311. this._queue = '';
  312. this._current_chart._began = false;
  313. if(netdata.options.DEBUG === true) netdata.debug('sent chart ' + this._current_chart.id);
  314. this._current_chart = null;
  315. return true;
  316. };
  317. // discard the collected values for the current chart - after calling begin()
  318. service.flush = function() {
  319. if(this._current_chart === null || this._current_chart._began === false) {
  320. this.error('Called flush() without an open chart.');
  321. return false;
  322. }
  323. this._queue = '';
  324. this._current_chart._began = false;
  325. this._current_chart = null;
  326. return true;
  327. };
  328. // create a netdata chart
  329. service.chart = function(id, chart) {
  330. var __DEBUG = netdata.options.DEBUG;
  331. if(typeof(netdata.charts[id]) === 'undefined') {
  332. netdata.charts[id] = {
  333. _created: false,
  334. _updated: true,
  335. _began: false,
  336. _counter: 0,
  337. _last_updated: 0,
  338. _dimensions_count: 0,
  339. id: id,
  340. name: id,
  341. title: 'untitled chart',
  342. units: 'a unit',
  343. family: '',
  344. context: '',
  345. type: netdata.chartTypes.line,
  346. priority: 50000,
  347. update_every: netdata.options.update_every,
  348. dimensions: {}
  349. };
  350. }
  351. var c = netdata.charts[id];
  352. if(typeof(chart.name) !== 'undefined' && chart.name !== c.name) {
  353. if(__DEBUG === true) netdata.debug('chart ' + id + ' updated its name');
  354. c.name = chart.name;
  355. c._updated = true;
  356. }
  357. if(typeof(chart.title) !== 'undefined' && chart.title !== c.title) {
  358. if(__DEBUG === true) netdata.debug('chart ' + id + ' updated its title');
  359. c.title = chart.title;
  360. c._updated = true;
  361. }
  362. if(typeof(chart.units) !== 'undefined' && chart.units !== c.units) {
  363. if(__DEBUG === true) netdata.debug('chart ' + id + ' updated its units');
  364. c.units = chart.units;
  365. c._updated = true;
  366. }
  367. if(typeof(chart.family) !== 'undefined' && chart.family !== c.family) {
  368. if(__DEBUG === true) netdata.debug('chart ' + id + ' updated its family');
  369. c.family = chart.family;
  370. c._updated = true;
  371. }
  372. if(typeof(chart.context) !== 'undefined' && chart.context !== c.context) {
  373. if(__DEBUG === true) netdata.debug('chart ' + id + ' updated its context');
  374. c.context = chart.context;
  375. c._updated = true;
  376. }
  377. if(typeof(chart.type) !== 'undefined' && chart.type !== c.type) {
  378. if(__DEBUG === true) netdata.debug('chart ' + id + ' updated its type');
  379. c.type = chart.type;
  380. c._updated = true;
  381. }
  382. if(typeof(chart.priority) !== 'undefined' && chart.priority !== c.priority) {
  383. if(__DEBUG === true) netdata.debug('chart ' + id + ' updated its priority');
  384. c.priority = chart.priority;
  385. c._updated = true;
  386. }
  387. if(typeof(chart.update_every) !== 'undefined' && chart.update_every !== c.update_every) {
  388. if(__DEBUG === true) netdata.debug('chart ' + id + ' updated its update_every from ' + c.update_every + ' to ' + chart.update_every);
  389. c.update_every = chart.update_every;
  390. c._updated = true;
  391. }
  392. if(typeof(chart.dimensions) !== 'undefined') {
  393. var dims = Object.keys(chart.dimensions);
  394. var len = dims.length;
  395. while(len--) {
  396. var x = dims[len];
  397. if(typeof(c.dimensions[x]) === 'undefined') {
  398. c._dimensions_count++;
  399. c.dimensions[x] = {
  400. _created: false,
  401. _updated: false,
  402. id: x, // the unique id of the dimension
  403. name: x, // the name of the dimension
  404. algorithm: netdata.chartAlgorithms.absolute, // the id of the netdata algorithm
  405. multiplier: 1, // the multiplier
  406. divisor: 1, // the divisor
  407. hidden: false // is hidden (boolean)
  408. };
  409. if(__DEBUG === true) netdata.debug('chart ' + id + ' created dimension ' + x);
  410. c._updated = true;
  411. }
  412. var dim = chart.dimensions[x];
  413. var d = c.dimensions[x];
  414. if(typeof(dim.name) !== 'undefined' && d.name !== dim.name) {
  415. if(__DEBUG === true) netdata.debug('chart ' + id + ', dimension ' + x + ' updated its name');
  416. d.name = dim.name;
  417. d._updated = true;
  418. }
  419. if(typeof(dim.algorithm) !== 'undefined' && d.algorithm !== dim.algorithm) {
  420. if(__DEBUG === true) netdata.debug('chart ' + id + ', dimension ' + x + ' updated its algorithm from ' + d.algorithm + ' to ' + dim.algorithm);
  421. d.algorithm = dim.algorithm;
  422. d._updated = true;
  423. }
  424. if(typeof(dim.multiplier) !== 'undefined' && d.multiplier !== dim.multiplier) {
  425. if(__DEBUG === true) netdata.debug('chart ' + id + ', dimension ' + x + ' updated its multiplier');
  426. d.multiplier = dim.multiplier;
  427. d._updated = true;
  428. }
  429. if(typeof(dim.divisor) !== 'undefined' && d.divisor !== dim.divisor) {
  430. if(__DEBUG === true) netdata.debug('chart ' + id + ', dimension ' + x + ' updated its divisor');
  431. d.divisor = dim.divisor;
  432. d._updated = true;
  433. }
  434. if(typeof(dim.hidden) !== 'undefined' && d.hidden !== dim.hidden) {
  435. if(__DEBUG === true) netdata.debug('chart ' + id + ', dimension ' + x + ' updated its hidden status');
  436. d.hidden = dim.hidden;
  437. d._updated = true;
  438. }
  439. if(d._updated) c._updated = true;
  440. }
  441. }
  442. //if(netdata.options.DEBUG === true) netdata.debug(netdata.charts);
  443. return netdata.charts[id];
  444. };
  445. return service;
  446. },
  447. runAllServices: function() {
  448. if(netdata.options.DEBUG === true) netdata.debug('runAllServices()');
  449. var now = Date.now();
  450. var len = netdata.services.length;
  451. while(len--) {
  452. var service = netdata.services[len];
  453. if(service.enabled === false || service.running === true) continue;
  454. if(now <= service.next_run) continue;
  455. service.update();
  456. now = Date.now();
  457. service.next_run = now - (now % (service.update_every * 1000)) + (service.update_every * 1000);
  458. }
  459. // 1/10th of update_every in pause
  460. setTimeout(netdata.runAllServices, netdata.options.update_every * 100);
  461. },
  462. start: function() {
  463. if(netdata.options.DEBUG === true) this.debug('started, services: ' + netdata.stringify(this.services));
  464. if(this.services.length === 0) {
  465. this.disableNodePlugin();
  466. // eslint suggested way to exit
  467. var exit = process.exit;
  468. exit(1);
  469. }
  470. else this.runAllServices();
  471. },
  472. // disable the whole node.js plugin
  473. disableNodePlugin: function() {
  474. this.send('DISABLE');
  475. // eslint suggested way to exit
  476. var exit = process.exit;
  477. exit(1);
  478. },
  479. requestFromParams: function(protocol, hostname, port, path, method) {
  480. return {
  481. protocol: protocol,
  482. hostname: hostname,
  483. port: port,
  484. path: path,
  485. //family: 4,
  486. method: method,
  487. headers: {
  488. 'Content-Type': 'application/x-www-form-urlencoded',
  489. 'Connection': 'keep-alive'
  490. },
  491. agent: new http.Agent({
  492. keepAlive: true,
  493. keepAliveMsecs: netdata.options.update_every * 1000,
  494. maxSockets: 2, // it must be 2 to work
  495. maxFreeSockets: 1
  496. })
  497. };
  498. },
  499. requestFromURL: function(a_url) {
  500. var u = url.parse(a_url);
  501. return netdata.requestFromParams(u.protocol, u.hostname, u.port, u.path, 'GET');
  502. },
  503. configure: function(module, config, callback) {
  504. if(netdata.options.DEBUG === true) this.debug(module.name + ': configuring (update_every: ' + this.options.update_every + ')...');
  505. module.active = 0;
  506. module.update_every = this.options.update_every;
  507. if(typeof config.update_every !== 'undefined')
  508. module.update_every = config.update_every;
  509. module.enable_autodetect = (config.enable_autodetect)?true:false;
  510. if(typeof(callback) === 'function')
  511. module.configure_callback = callback;
  512. else
  513. module.configure_callback = null;
  514. var added = module.configure(config);
  515. if(netdata.options.DEBUG === true) this.debug(module.name + ': configured, reporting ' + added + ' eligible services.');
  516. if(module.configure_callback !== null && added === 0) {
  517. if(netdata.options.DEBUG === true) this.debug(module.name + ': configuration finish callback called from configure().');
  518. var configure_callback = module.configure_callback;
  519. module.configure_callback = null;
  520. configure_callback();
  521. }
  522. return added;
  523. }
  524. };
  525. if(netdata.options.DEBUG === true) netdata.debug('loaded netdata from:', __filename);
  526. module.exports = netdata;