netdata.js 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654
  1. 'use strict';
  2. // netdata
  3. // real-time performance and health monitoring, done right!
  4. // (C) 2016 Costa Tsaousis <costa@tsaousis.gr>
  5. // SPDX-License-Identifier: GPL-3.0-or-later
  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. try {
  198. responseProcessor(service, response);
  199. }
  200. catch(e) {
  201. netdata.error(e);
  202. service.error("responseProcessor failed process response data.");
  203. }
  204. service.running = false;
  205. service.module.active--;
  206. if(service.module.active < 0) {
  207. service.module.active = 0;
  208. if(__DEBUG === true)
  209. netdata.debug(service.module.name + ': active module counter below zero.');
  210. }
  211. if(service.module.active === 0) {
  212. // check if we run under configure
  213. if(service.module.configure_callback !== null) {
  214. if(__DEBUG === true)
  215. netdata.debug(service.module.name + ': configuration finish callback called from processResponse().');
  216. var configure_callback = service.module.configure_callback;
  217. service.module.configure_callback = null;
  218. configure_callback();
  219. }
  220. }
  221. });
  222. };
  223. service.update = function() {
  224. if(netdata.options.DEBUG === true)
  225. netdata.debug(this.module.name + ': ' + this.name + ': starting data collection...');
  226. this.module.update(this, function() {
  227. if(netdata.options.DEBUG === true)
  228. netdata.debug(service.module.name + ': ' + service.name + ': data collection ended in ' + service.duration.toString() + ' ms.');
  229. });
  230. };
  231. service.error = function(message) {
  232. if(this.error_reported === false) {
  233. netdata.error(this.module.name + ': ' + this.name + ': ' + message);
  234. this.error_reported = true;
  235. }
  236. else if(netdata.options.DEBUG === true)
  237. netdata.debug(this.module.name + ': ' + this.name + ': ' + message);
  238. };
  239. service.errorClear = function() {
  240. this.error_reported = false;
  241. };
  242. service.queue = function(txt) {
  243. this._queue += txt + '\n';
  244. };
  245. service._send_chart_to_netdata = function(chart) {
  246. // internal function to send a chart to netdata
  247. this.queue('CHART "' + chart.id + '" "' + chart.name + '" "' + chart.title + '" "' + chart.units + '" "' + chart.family + '" "' + chart.context + '" "' + chart.type + '" ' + chart.priority.toString() + ' ' + chart.update_every.toString());
  248. if(typeof(chart.dimensions) !== 'undefined') {
  249. var dims = Object.keys(chart.dimensions);
  250. var len = dims.length;
  251. while(len--) {
  252. var d = chart.dimensions[dims[len]];
  253. this.queue('DIMENSION "' + d.id + '" "' + d.name + '" "' + d.algorithm + '" ' + d.multiplier.toString() + ' ' + d.divisor.toString() + ' ' + ((d.hidden === true) ? 'hidden' : '').toString());
  254. d._created = true;
  255. d._updated = false;
  256. }
  257. }
  258. chart._created = true;
  259. chart._updated = false;
  260. };
  261. // begin data collection for a chart
  262. service.begin = function(chart) {
  263. if(this._current_chart !== null && this._current_chart !== chart) {
  264. this.error('Called begin() for chart ' + chart.id + ' while chart ' + this._current_chart.id + ' is still open. Closing it.');
  265. this.end();
  266. }
  267. if(typeof(chart.id) === 'undefined' || netdata.charts[chart.id] !== chart) {
  268. this.error('Called begin() for chart ' + chart.id + ' that is not mine. Where did you find it? Ignoring it.');
  269. return false;
  270. }
  271. if(netdata.options.DEBUG === true) netdata.debug('setting current chart to ' + chart.id);
  272. this._current_chart = chart;
  273. this._current_chart._began = true;
  274. if(this._current_chart._dimensions_count !== 0) {
  275. if(this._current_chart._created === false || this._current_chart._updated === true)
  276. this._send_chart_to_netdata(this._current_chart);
  277. var now = this.ended;
  278. this.queue('BEGIN ' + this._current_chart.id + ' ' + ((this._current_chart._last_updated > 0)?((now - this._current_chart._last_updated) * 1000):'').toString());
  279. }
  280. // else this.error('Called begin() for chart ' + chart.id + ' which is empty.');
  281. this._current_chart._last_updated = now;
  282. this._current_chart._began = true;
  283. this._current_chart._counter++;
  284. return true;
  285. };
  286. // set a collected value for a chart
  287. // we do most things on the first value we attempt to set
  288. service.set = function(dimension, value) {
  289. if(this._current_chart === null) {
  290. this.error('Called set(' + dimension + ', ' + value + ') without an open chart.');
  291. return false;
  292. }
  293. if(typeof(this._current_chart.dimensions[dimension]) === 'undefined') {
  294. this.error('Called set(' + dimension + ', ' + value + ') but dimension "' + dimension + '" does not exist in chart "' + this._current_chart.id + '".');
  295. return false;
  296. }
  297. if(typeof value === 'undefined' || value === null)
  298. return false;
  299. if(this._current_chart._dimensions_count !== 0)
  300. this.queue('SET ' + dimension + ' = ' + value.toString());
  301. return true;
  302. };
  303. // end data collection for the current chart - after calling begin()
  304. service.end = function() {
  305. if(this._current_chart !== null && this._current_chart._began === false) {
  306. this.error('Called end() without an open chart.');
  307. return false;
  308. }
  309. if(this._current_chart._dimensions_count !== 0) {
  310. this.queue('END');
  311. netdata.send(this._queue);
  312. }
  313. this._queue = '';
  314. this._current_chart._began = false;
  315. if(netdata.options.DEBUG === true) netdata.debug('sent chart ' + this._current_chart.id);
  316. this._current_chart = null;
  317. return true;
  318. };
  319. // discard the collected values for the current chart - after calling begin()
  320. service.flush = function() {
  321. if(this._current_chart === null || this._current_chart._began === false) {
  322. this.error('Called flush() without an open chart.');
  323. return false;
  324. }
  325. this._queue = '';
  326. this._current_chart._began = false;
  327. this._current_chart = null;
  328. return true;
  329. };
  330. // create a netdata chart
  331. service.chart = function(id, chart) {
  332. var __DEBUG = netdata.options.DEBUG;
  333. if(typeof(netdata.charts[id]) === 'undefined') {
  334. netdata.charts[id] = {
  335. _created: false,
  336. _updated: true,
  337. _began: false,
  338. _counter: 0,
  339. _last_updated: 0,
  340. _dimensions_count: 0,
  341. id: id,
  342. name: id,
  343. title: 'untitled chart',
  344. units: 'a unit',
  345. family: '',
  346. context: '',
  347. type: netdata.chartTypes.line,
  348. priority: 50000,
  349. update_every: netdata.options.update_every,
  350. dimensions: {}
  351. };
  352. }
  353. var c = netdata.charts[id];
  354. if(typeof(chart.name) !== 'undefined' && chart.name !== c.name) {
  355. if(__DEBUG === true) netdata.debug('chart ' + id + ' updated its name');
  356. c.name = chart.name;
  357. c._updated = true;
  358. }
  359. if(typeof(chart.title) !== 'undefined' && chart.title !== c.title) {
  360. if(__DEBUG === true) netdata.debug('chart ' + id + ' updated its title');
  361. c.title = chart.title;
  362. c._updated = true;
  363. }
  364. if(typeof(chart.units) !== 'undefined' && chart.units !== c.units) {
  365. if(__DEBUG === true) netdata.debug('chart ' + id + ' updated its units');
  366. c.units = chart.units;
  367. c._updated = true;
  368. }
  369. if(typeof(chart.family) !== 'undefined' && chart.family !== c.family) {
  370. if(__DEBUG === true) netdata.debug('chart ' + id + ' updated its family');
  371. c.family = chart.family;
  372. c._updated = true;
  373. }
  374. if(typeof(chart.context) !== 'undefined' && chart.context !== c.context) {
  375. if(__DEBUG === true) netdata.debug('chart ' + id + ' updated its context');
  376. c.context = chart.context;
  377. c._updated = true;
  378. }
  379. if(typeof(chart.type) !== 'undefined' && chart.type !== c.type) {
  380. if(__DEBUG === true) netdata.debug('chart ' + id + ' updated its type');
  381. c.type = chart.type;
  382. c._updated = true;
  383. }
  384. if(typeof(chart.priority) !== 'undefined' && chart.priority !== c.priority) {
  385. if(__DEBUG === true) netdata.debug('chart ' + id + ' updated its priority');
  386. c.priority = chart.priority;
  387. c._updated = true;
  388. }
  389. if(typeof(chart.update_every) !== 'undefined' && chart.update_every !== c.update_every) {
  390. if(__DEBUG === true) netdata.debug('chart ' + id + ' updated its update_every from ' + c.update_every + ' to ' + chart.update_every);
  391. c.update_every = chart.update_every;
  392. c._updated = true;
  393. }
  394. if(typeof(chart.dimensions) !== 'undefined') {
  395. var dims = Object.keys(chart.dimensions);
  396. var len = dims.length;
  397. while(len--) {
  398. var x = dims[len];
  399. if(typeof(c.dimensions[x]) === 'undefined') {
  400. c._dimensions_count++;
  401. c.dimensions[x] = {
  402. _created: false,
  403. _updated: false,
  404. id: x, // the unique id of the dimension
  405. name: x, // the name of the dimension
  406. algorithm: netdata.chartAlgorithms.absolute, // the id of the netdata algorithm
  407. multiplier: 1, // the multiplier
  408. divisor: 1, // the divisor
  409. hidden: false // is hidden (boolean)
  410. };
  411. if(__DEBUG === true) netdata.debug('chart ' + id + ' created dimension ' + x);
  412. c._updated = true;
  413. }
  414. var dim = chart.dimensions[x];
  415. var d = c.dimensions[x];
  416. if(typeof(dim.name) !== 'undefined' && d.name !== dim.name) {
  417. if(__DEBUG === true) netdata.debug('chart ' + id + ', dimension ' + x + ' updated its name');
  418. d.name = dim.name;
  419. d._updated = true;
  420. }
  421. if(typeof(dim.algorithm) !== 'undefined' && d.algorithm !== dim.algorithm) {
  422. if(__DEBUG === true) netdata.debug('chart ' + id + ', dimension ' + x + ' updated its algorithm from ' + d.algorithm + ' to ' + dim.algorithm);
  423. d.algorithm = dim.algorithm;
  424. d._updated = true;
  425. }
  426. if(typeof(dim.multiplier) !== 'undefined' && d.multiplier !== dim.multiplier) {
  427. if(__DEBUG === true) netdata.debug('chart ' + id + ', dimension ' + x + ' updated its multiplier');
  428. d.multiplier = dim.multiplier;
  429. d._updated = true;
  430. }
  431. if(typeof(dim.divisor) !== 'undefined' && d.divisor !== dim.divisor) {
  432. if(__DEBUG === true) netdata.debug('chart ' + id + ', dimension ' + x + ' updated its divisor');
  433. d.divisor = dim.divisor;
  434. d._updated = true;
  435. }
  436. if(typeof(dim.hidden) !== 'undefined' && d.hidden !== dim.hidden) {
  437. if(__DEBUG === true) netdata.debug('chart ' + id + ', dimension ' + x + ' updated its hidden status');
  438. d.hidden = dim.hidden;
  439. d._updated = true;
  440. }
  441. if(d._updated) c._updated = true;
  442. }
  443. }
  444. //if(netdata.options.DEBUG === true) netdata.debug(netdata.charts);
  445. return netdata.charts[id];
  446. };
  447. return service;
  448. },
  449. runAllServices: function() {
  450. if(netdata.options.DEBUG === true) netdata.debug('runAllServices()');
  451. var now = Date.now();
  452. var len = netdata.services.length;
  453. while(len--) {
  454. var service = netdata.services[len];
  455. if(service.enabled === false || service.running === true) continue;
  456. if(now <= service.next_run) continue;
  457. service.update();
  458. now = Date.now();
  459. service.next_run = now - (now % (service.update_every * 1000)) + (service.update_every * 1000);
  460. }
  461. // 1/10th of update_every in pause
  462. setTimeout(netdata.runAllServices, netdata.options.update_every * 100);
  463. },
  464. start: function() {
  465. if(netdata.options.DEBUG === true) this.debug('started, services: ' + netdata.stringify(this.services));
  466. if(this.services.length === 0) {
  467. this.disableNodePlugin();
  468. // eslint suggested way to exit
  469. var exit = process.exit;
  470. exit(1);
  471. }
  472. else this.runAllServices();
  473. },
  474. // disable the whole node.js plugin
  475. disableNodePlugin: function() {
  476. this.send('DISABLE');
  477. // eslint suggested way to exit
  478. var exit = process.exit;
  479. exit(1);
  480. },
  481. requestFromParams: function(protocol, hostname, port, path, method) {
  482. return {
  483. protocol: protocol,
  484. hostname: hostname,
  485. port: port,
  486. path: path,
  487. //family: 4,
  488. method: method,
  489. headers: {
  490. 'Content-Type': 'application/x-www-form-urlencoded',
  491. 'Connection': 'keep-alive'
  492. },
  493. agent: new http.Agent({
  494. keepAlive: true,
  495. keepAliveMsecs: netdata.options.update_every * 1000,
  496. maxSockets: 2, // it must be 2 to work
  497. maxFreeSockets: 1
  498. })
  499. };
  500. },
  501. requestFromURL: function(a_url) {
  502. var u = url.parse(a_url);
  503. return netdata.requestFromParams(u.protocol, u.hostname, u.port, u.path, 'GET');
  504. },
  505. configure: function(module, config, callback) {
  506. if(netdata.options.DEBUG === true) this.debug(module.name + ': configuring (update_every: ' + this.options.update_every + ')...');
  507. module.active = 0;
  508. module.update_every = this.options.update_every;
  509. if(typeof config.update_every !== 'undefined')
  510. module.update_every = config.update_every;
  511. module.enable_autodetect = (config.enable_autodetect)?true:false;
  512. if(typeof(callback) === 'function')
  513. module.configure_callback = callback;
  514. else
  515. module.configure_callback = null;
  516. var added = module.configure(config);
  517. if(netdata.options.DEBUG === true) this.debug(module.name + ': configured, reporting ' + added + ' eligible services.');
  518. if(module.configure_callback !== null && added === 0) {
  519. if(netdata.options.DEBUG === true) this.debug(module.name + ': configuration finish callback called from configure().');
  520. var configure_callback = module.configure_callback;
  521. module.configure_callback = null;
  522. configure_callback();
  523. }
  524. return added;
  525. }
  526. };
  527. if(netdata.options.DEBUG === true) netdata.debug('loaded netdata from:', __filename);
  528. module.exports = netdata;