munin_api.module 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625
  1. <?php
  2. /**
  3. * @file
  4. * Munin API for Drupal.
  5. *
  6. * @copyright (c) 2011-2019 Ouest Systèmes Informatiques
  7. *
  8. * Licensed under the General Public License version 2 or later.
  9. *
  10. * TODO Field name should be sanitized by "s/[^A-Za-z0-9_]/_/g"
  11. * @link http://munin-monitoring.org/wiki/notes_on_datasource_names @endlink
  12. */
  13. use Drupal\munin_api\Munin;
  14. require_once __DIR__ . '/src/Munin.php';
  15. define('MUNIN_API_COUNTER', 'COUNTER');
  16. define('MUNIN_API_DERIVE', 'DERIVE');
  17. define('MUNIN_API_GAUGE', 'GAUGE');
  18. // Counters reset upon reading (uncommon).
  19. define('MUNIN_API_ABSOLUTE', 'ABSOLUTE');
  20. define('MUNIN_API_DRAW_AREA', 'AREA');
  21. // Invisible line, but triggers graphs scaling.
  22. define('MUNIN_API_DRAW_LINE0', 'LINE0');
  23. // Default on Munin 2.0.
  24. define('MUNIN_API_DRAW_LINE1', 'LINE1');
  25. // Default on Munin < 2.0.
  26. define('MUNIN_API_DRAW_LINE2', 'LINE2');
  27. define('MUNIN_API_DRAW_LINE3', 'LINE3');
  28. define('MUNIN_API_DRAW_STACK', 'STACK');
  29. // Styles below are only supported on Munin >= 1.3.3.
  30. define('MUNIN_API_DRAW_LINESTACK1', 'LINESTACK1');
  31. define('MUNIN_API_DRAW_LINESTACK2', 'LINESTACK2');
  32. define('MUNIN_API_DRAW_LINESTACK3', 'LINESTACK3');
  33. define('MUNIN_API_DRAW_AREASTACK', 'AREASTACK');
  34. // Features server for plugin stats.
  35. define('MUNIN_API_SERVER_HOST', 'http://features.osinet.eu/');
  36. define('MUNIN_API_SERVER_ENDPOINT', 'xmlrpc.php');
  37. /**
  38. * Menu access callback for Munin config fetches.
  39. *
  40. * TODO: define rules, then code. For now, protect at the web server level.
  41. */
  42. function _munin_api_access_config($module) {
  43. return TRUE;
  44. }
  45. /**
  46. * Menu access callback for Munin data fetches.
  47. *
  48. * TODO: define rules, then code. For now, protect at the web server level.
  49. */
  50. function _munin_api_access_fetch($module) {
  51. return TRUE;
  52. }
  53. /**
  54. * Build the site information to report on the plugin popularity page.
  55. *
  56. * The site key is a reasonably unique host identifier obtained without
  57. * exposing private info. This is the same method as in core update module.
  58. *
  59. * @see _update_process_fetch_task()
  60. *
  61. * @return array
  62. * The datestamp and version keys default to 0 if the information is not
  63. * available, as happens with Git-based deployments.
  64. */
  65. function _munin_api_get_report_info() {
  66. $module_info = drupal_parse_info_file(drupal_get_path('module', Munin::MODULE)
  67. . '/' . Munin::MODULE . '.info');
  68. $plugins = module_implements(Munin::HOOK_INFO);
  69. $hidden = array(Munin::MODULE);
  70. foreach ($plugins as $plugin_name) {
  71. $plugin_info = module_invoke($plugin_name, Munin::HOOK_INFO);
  72. if (!empty($plugin_info['#private'])) {
  73. $hidden[] = $plugin_name;
  74. }
  75. }
  76. $reported = array_diff($plugins, $hidden);
  77. $ret = array(
  78. 'module' => Munin::MODULE,
  79. 'datestamp' => check_plain($module_info['datestamp'] ?? 0),
  80. 'plugins' => $reported,
  81. 'site-key' => drupal_hmac_base64($GLOBALS['base_url'], drupal_get_private_key()),
  82. 'version' => check_plain($module_info['version'] ?? '0'),
  83. );
  84. return $ret;
  85. }
  86. /**
  87. * Finalize result pages for Munin interactions.
  88. *
  89. * - text content, not HTML,
  90. * - non cacheable, even for anonymous users.
  91. */
  92. function _munin_api_page_closure($ret) {
  93. drupal_add_http_header('Content-type', 'text/plain');
  94. // Prevent drupal_page_set_cache() in drupal_page_footer().
  95. $GLOBALS['conf']['cache'] = 0;
  96. print $ret;
  97. drupal_page_footer();
  98. exit();
  99. }
  100. /**
  101. * Display and log an incorrect hook implementation.
  102. *
  103. * @param string $hook
  104. * The hook in which the error was triggered.
  105. * @param string $module
  106. * The module implementing the hook in which the error was triggered.
  107. *
  108. * @return string
  109. * An error string suitable for use as a controller result.
  110. */
  111. function _munin_report_hook_error($hook, $module) {
  112. $message = t('Incorrect implementation of hook_!hook() in module @module.');
  113. $params = array(
  114. '!hook' => $hook,
  115. '@module' => $module,
  116. );
  117. drupal_set_message(format_string($message, $params), 'error');
  118. watchdog(Munin::MODULE, $message, $params, WATCHDOG_ERROR);
  119. return '<p>' . l(t('Back'), Munin::R_REPORTS . "/${$module}") . '</p>';
  120. }
  121. /**
  122. * Munin API watchdog implementation.
  123. *
  124. * Increase severity if proves are too frequent, liable to cause extra load on
  125. * the site.
  126. */
  127. function _munin_api_watchdog_munin() {
  128. $seconds = munin_api_munin_api_fetch(Munin::PROBE_MUNIN, FALSE);
  129. $seconds = reset($seconds);
  130. $info = munin_api_munin_api_info();
  131. $info = $info[Munin::PROBE_MUNIN]['munin_seconds'];
  132. $warning = explode(':', $info['#warning']);
  133. $critical = explode(':', $info['#critical']);
  134. if (/* $seconds < $critical[0] || */ $seconds > $critical[1]) {
  135. $level = WATCHDOG_CRITICAL;
  136. }
  137. elseif (/* $seconds < $warning[0] || */ $seconds > $warning[1]) {
  138. $level = WATCHDOG_WARNING;
  139. }
  140. else {
  141. $level = WATCHDOG_DEBUG;
  142. }
  143. // Meaning MORE urgent than debug.
  144. if ($level < WATCHDOG_DEBUG) {
  145. watchdog(Munin::MODULE, 'Munin last probe came @last seconds ago.', [
  146. '@last' => $seconds,
  147. ], $level);
  148. }
  149. }
  150. /**
  151. * Form build for admin/config/munin_api.
  152. *
  153. * @param array $form_state
  154. * The form state.
  155. *
  156. * @return array
  157. * A form array.
  158. *
  159. * @throws \Exception
  160. */
  161. function munin_api_admin_settings(array $form_state) {
  162. $default_path = variable_get(Munin::V_INIT_PATH, Munin::D_INIT_PATH);
  163. $form = [];
  164. // Add the fade-in/out effects.
  165. drupal_add_js(drupal_get_path('module', Munin::MODULE) . '/munin_api.js');
  166. drupal_add_js([
  167. Munin::MODULE => [
  168. 'path' => $default_path,
  169. ],
  170. ], [
  171. 'type' => 'setting',
  172. 'scope' => JS_DEFAULT,
  173. ]);
  174. $form['watchdog'] = [
  175. '#type' => 'fieldset',
  176. '#title' => t('Munin Watchdog'),
  177. '#description' => t('Drupal-level monitoring of Munin'),
  178. ];
  179. $form['watchdog'][Munin::V_WATCHDOG] = [
  180. '#description' => t('If enabled, the module will regularly make sure the time interval between Munin probes is within the expected range and raise watchdog alerts accordingly. "Cron" mode is recommended. "Page init" mode should only be used on pages with a steady occurrence rate below 5 minutes but not too high, and will not work properly for anonymous users if <a href="!performance">expiration of cached pages</a> is set to a non-zero value. This mechanism is most useful when watchdog is implemented by syslog, not dblog.', [
  181. '!performance' => url('admin/config/development/performance'),
  182. ]),
  183. '#type' => 'radios',
  184. '#options' => [
  185. Munin::V_WATCHDOG_NONE => t('Disabled'),
  186. Munin::V_WATCHDOG_CRON => t('On cron runs'),
  187. Munin::V_WATCHDOG_INIT => t('On page init'),
  188. ],
  189. '#default_value' => variable_get(Munin::V_WATCHDOG, Munin::D_WATCHDOG),
  190. ];
  191. $form['watchdog'][Munin::V_INIT_PATH] = [
  192. '#title' => t('Init path'),
  193. '#description' => t('For page init watchdog mode, specify the regular expression for the paths that should trigger a watchdog check'),
  194. '#type' => 'textfield',
  195. '#default_value' => $default_path,
  196. ];
  197. $form['reporting'] = [
  198. '#type' => 'fieldset',
  199. '#title' => t('Module reporting'),
  200. '#description' => t('Information reported by this module to the <a href="!ofs">OSInet Features Server</a>', [
  201. '!ofs' => 'http://features.osinet.eu/munin-api-drupal',
  202. ]),
  203. ];
  204. $form['reporting'][Munin::V_NEXT_REPORT] = [
  205. '#type' => 'checkbox',
  206. '#title' => t('Report anonymous usage statistics'),
  207. '#default_value' => variable_get(Munin::V_NEXT_REPORT, 0),
  208. '#return_value' => REQUEST_TIME,
  209. '#description' => t('Opt-in for reporting usage data about Munin plugins without breaking the anonymity of the site. See the reported information below.'),
  210. ];
  211. $info = _munin_api_get_report_info();
  212. $header = [
  213. t('Property'),
  214. t('Value'),
  215. t('Explanation'),
  216. ];
  217. $rows = [
  218. [
  219. 'module',
  220. $info['module'],
  221. t('The project name of this module'),
  222. ],
  223. [
  224. 'version',
  225. $info['version'],
  226. t('The official version of this module'),
  227. ],
  228. [
  229. 'datestamp',
  230. $info['datestamp'],
  231. t('Consistency check for the version'),
  232. ],
  233. [
  234. 'site-key',
  235. $info['site-key'],
  236. t('Your site unique key. This is the same identifier you are sending to drupal.org via update.module: it provides a reasonably unique identifier to count sites without reporting any identifiable information.'),
  237. ],
  238. [
  239. 'plugins',
  240. theme('item_list', ['items' => $info['plugins']]),
  241. t('This shows which are the most popular plugins. Plugins with the #private attribute are considered private and not reported here.'),
  242. ],
  243. ];
  244. $form['reporting']['info'] = [
  245. '#theme' => 'table',
  246. '#header' => $header,
  247. '#rows' => $rows,
  248. '#attributes' => ['class' => ['munin-api-report']],
  249. ];
  250. $form = system_settings_form($form);
  251. return $form;
  252. }
  253. /**
  254. * Implements hook_cron().
  255. */
  256. function munin_api_cron() {
  257. // Is the Munin-node watchdog enabled in cron mode ?
  258. if (variable_get(Munin::V_WATCHDOG, Munin::D_WATCHDOG) == Munin::V_WATCHDOG_CRON) {
  259. _munin_api_watchdog_munin();
  260. }
  261. // Has the admin opted in for the anonymous usage report ?
  262. if ($next = variable_get(Munin::V_NEXT_REPORT, 0)) {
  263. if (REQUEST_TIME > $next) {
  264. $link = l(t('Browse server'), MUNIN_API_SERVER_HOST, ['external' => TRUE]);
  265. $ret = xmlrpc(MUNIN_API_SERVER_HOST . MUNIN_API_SERVER_ENDPOINT,
  266. 'ofe.usage', _munin_api_get_report_info());
  267. if ($ret === FALSE) {
  268. watchdog(Munin::MODULE, 'XML-RPC error @errno: @message', array(
  269. '@errno' => xmlrpc_errno(),
  270. '@message' => xmlrpc_error_msg(),
  271. ), WATCHDOG_NOTICE, $link);
  272. }
  273. else {
  274. watchdog(Munin::MODULE, 'Plugin usage statistics uploaded', NULL,
  275. WATCHDOG_INFO, $link);
  276. // Set next update from current value of next, not from current time,
  277. // to avoid slow shifts over time. 604800 = 7 days.
  278. variable_set(Munin::V_NEXT_REPORT, $next + 604800);
  279. }
  280. }
  281. }
  282. }
  283. /**
  284. * Implements hook_help().
  285. */
  286. function munin_api_help($path, $arg) {
  287. switch ($path) {
  288. case 'admin/help#munin_api':
  289. if (empty($arg[0])) {
  290. return '<p>' . t('Most of the help for the Munin API modules is in <a href="!link">Advanced Help</a>', [
  291. '!link' => url('admin/help/ah/munin_api'),
  292. ]) . '</p>';
  293. }
  294. break;
  295. }
  296. }
  297. /**
  298. * Implements hook_init().
  299. *
  300. * Only trigger Munin API watchdog on selected pages, and won't work on cached
  301. * pages unless $conf['page_cache_invoke_hooks'] is TRUE.
  302. *
  303. * @see _drupal_bootstrap_page_cache()
  304. */
  305. function munin_api_init() {
  306. if (variable_get(Munin::V_WATCHDOG, Munin::D_WATCHDOG) == Munin::V_WATCHDOG_INIT) {
  307. $path = $_GET['q'];
  308. $regex = variable_get(Munin::V_INIT_PATH, Munin::D_INIT_PATH);
  309. $sts = preg_match($regex, $path);
  310. if ($sts) {
  311. _munin_api_watchdog_munin();
  312. }
  313. }
  314. }
  315. /**
  316. * Implements hook_menu().
  317. */
  318. function munin_api_menu() {
  319. $items = [];
  320. $items[Munin::R_CONFIG] = [
  321. 'title' => 'Munin',
  322. 'description' => 'Munin API settings',
  323. 'page callback' => 'drupal_get_form',
  324. 'page arguments' => ['munin_api_admin_settings'],
  325. 'access arguments' => ['administer site configuration'],
  326. ];
  327. $items[Munin::R_REPORTS] = [
  328. 'title' => 'Munin',
  329. 'description' => 'Reports about Munin data collectors and their probes',
  330. 'page callback' => 'munin_api_page_report_global',
  331. 'access arguments' => ['access site reports'],
  332. ];
  333. $items[Munin::R_REPORTS . '/list'] = [
  334. 'type' => MENU_DEFAULT_LOCAL_TASK,
  335. 'title' => 'General',
  336. 'weight' => -1,
  337. ];
  338. foreach (module_implements(Munin::HOOK_INFO) as $module_name) {
  339. $module = module_invoke($module_name, Munin::HOOK_INFO);
  340. $graphs = element_children($module);
  341. $items[Munin::R_REPORTS . "/${module_name}"] = [
  342. 'type' => MENU_LOCAL_TASK,
  343. 'title' => $module['#title'],
  344. 'description' => $module['#description'],
  345. 'page callback' => 'munin_api_page_report_instance',
  346. 'page arguments' => [$module_name, $module],
  347. 'access arguments' => ['access site reports'],
  348. ];
  349. foreach ($graphs as $graph_name) {
  350. $items[Munin::R_BASE . "/${graph_name}"] = [
  351. 'type' => MENU_CALLBACK,
  352. 'page callback' => 'munin_api_page_fetch',
  353. 'page arguments' => [$module_name, $module, $graph_name],
  354. 'access callback' => '_munin_api_access_fetch',
  355. 'access arguments' => [$module_name, $graph_name],
  356. ];
  357. $items[Munin::R_BASE . "/${graph_name}/config"] = [
  358. 'type' => MENU_CALLBACK,
  359. 'page callback' => 'munin_api_page_config',
  360. 'page arguments' => [$module_name, $graph_name],
  361. 'access callback' => '_munin_api_access_config',
  362. 'access arguments' => [$module_name, $graph_name],
  363. ];
  364. }
  365. }
  366. ksort($items);
  367. return $items;
  368. }
  369. /**
  370. * Implements hook_munin_api_fetch().
  371. */
  372. function munin_api_munin_api_fetch($graph_name, $log = TRUE) {
  373. switch ($graph_name) {
  374. case Munin::PROBE_MUNIN:
  375. $requestTime = REQUEST_TIME;
  376. $last = variable_get(Munin::V_LAST, 0);
  377. $ret['munin_seconds'] = $last ? $requestTime - $last : 0;
  378. if ($log) {
  379. variable_set(Munin::V_LAST, $requestTime);
  380. }
  381. break;
  382. }
  383. return $ret;
  384. }
  385. /**
  386. * Implements hook_munin_api_info().
  387. *
  388. * Returns an array of Munin probes information, indexed by probe name.
  389. */
  390. function munin_api_munin_api_info() {
  391. $int = [
  392. '#graph_printf' => "'%d'",
  393. ];
  394. $ret = [
  395. '#title' => t('Munin'),
  396. '#description' => t('Munin monitoring'),
  397. Munin::PROBE_MUNIN => [
  398. '#title' => t('Munin-node'),
  399. '#info' => t('Monitor the monitoring solution.'),
  400. '#graph_vlabel' => t('Seconds since last probe'),
  401. 'munin_seconds' => $int + [
  402. '#label' => t('Seconds since last probe'),
  403. '#type' => MUNIN_API_GAUGE,
  404. '#info' => t('Seconds since last probe should hover around 300. 0 means no probe found.'),
  405. '#warning' => '290:310',
  406. '#critical' => '1:600',
  407. ],
  408. ],
  409. ];
  410. return $ret;
  411. }
  412. /**
  413. * Page callback for munin config fetches.
  414. */
  415. function munin_api_page_config($module_name, $graph_name) {
  416. $module_info = module_invoke($module_name, Munin::HOOK_INFO);
  417. if (!is_array($module_info)) {
  418. return _munin_report_hook_error(Munin::HOOK_INFO, $module_name);
  419. }
  420. $info = $module_info[$graph_name];
  421. $config = array(
  422. 'graph_title' => $info['#title'],
  423. 'graph_info' => $info['#info'],
  424. 'graph_category' => 'Drupal',
  425. );
  426. foreach (element_properties($info) as $property_name) {
  427. if (!in_array($property_name, array('#title', '#info'))) {
  428. $config[drupal_substr($property_name, 1)] = $info[$property_name];
  429. }
  430. }
  431. foreach (element_children($info) as $field_name) {
  432. foreach (element_properties($info[$field_name]) as $property_name) {
  433. $config[$field_name . '.' . drupal_substr($property_name, 1)] = $info[$field_name][$property_name];
  434. }
  435. }
  436. $ret = '';
  437. foreach ($config as $k => $v) {
  438. $ret .= $k . ' ' . $v . PHP_EOL;
  439. }
  440. _munin_api_page_closure($ret);
  441. }
  442. /**
  443. * Page callback for Munin data fetches.
  444. */
  445. function munin_api_page_fetch($module_name, $module, $graph_name) {
  446. $data = module_invoke($module_name, Munin::HOOK_FETCH, $graph_name);
  447. if (!is_array($data)) {
  448. return _munin_report_hook_error(Munin::HOOK_FETCH, $module_name);
  449. }
  450. $ret = '';
  451. foreach ($data as $field => $value) {
  452. $ret .= $field . '.value ' . $value . PHP_EOL;
  453. }
  454. _munin_api_page_closure($ret);
  455. }
  456. /**
  457. * Page callback for Munin global report.
  458. *
  459. * @return array
  460. * A render array for the page contents.
  461. *
  462. * @throws \Exception
  463. */
  464. function munin_api_page_report_global() {
  465. $header = [
  466. t('Module'),
  467. t('Graph'),
  468. t('Description'),
  469. t('Fields'),
  470. ];
  471. $rows = [];
  472. foreach (module_implements(Munin::HOOK_INFO) as $module_name) {
  473. $info = module_invoke($module_name, Munin::HOOK_INFO);
  474. $rows[] = [
  475. [
  476. 'data' => l($info['#title'] ?? $module_name, Munin::R_REPORTS . "/${module_name}"),
  477. 'colspan' => 2,
  478. ],
  479. [
  480. 'colspan' => 2,
  481. 'data' => $info['#description'] ?? '',
  482. ],
  483. ];
  484. foreach (element_children($info) as $name) {
  485. $title = $info[$name]['#title'] ?? $name;
  486. $rows[] = [
  487. '&nbsp;',
  488. $title,
  489. isset($info[$name]['#info']) ?? t('&lt;missing&gt;'),
  490. count(element_children($info[$name])),
  491. ];
  492. }
  493. }
  494. $ret = [
  495. '#theme' => 'table',
  496. '#header' => $header,
  497. '#rows' => $rows,
  498. ];
  499. return $ret;
  500. }
  501. /**
  502. * Page callback for Munin instance report.
  503. *
  504. * @param string $module_name
  505. * The name of the module providing Munin data.
  506. * @param array $module_info
  507. * The information about the module.
  508. *
  509. * @return array
  510. * A render array for the report.
  511. *
  512. * @throws \Exception
  513. */
  514. function munin_api_page_report_instance($module_name, array $module_info) {
  515. $header = [
  516. t('Name'),
  517. t('Title / Description'),
  518. t('Type'),
  519. t('Debug'),
  520. ];
  521. $error = ['class' => 'error'];
  522. $rows = [];
  523. foreach (element_children($module_info) as $graph_name) {
  524. $data = module_invoke($module_name, Munin::HOOK_FETCH, $graph_name, FALSE);
  525. if (!is_array($data)) {
  526. return _munin_report_hook_error(Munin::HOOK_FETCH, $module_name);
  527. }
  528. $rows[] = [
  529. [
  530. 'data' => $module_info[$graph_name]['#title'] ?? $graph_name,
  531. 'colspan' => 3,
  532. ],
  533. l(t('config'), Munin::R_BASE . "/${graph_name}/config"),
  534. ];
  535. foreach (element_children($module_info[$graph_name]) as $field_name) {
  536. $rows[] = [
  537. '&nbsp;',
  538. $module_info[$graph_name][$field_name]['#label'] ?? t('&lt;missing&gt;'),
  539. $module_info[$graph_name][$field_name]['#type'],
  540. $data[$field_name],
  541. ];
  542. unset($data[$field_name]);
  543. }
  544. foreach ($data as $field_name => $field_value) {
  545. $rows[] = [
  546. '&nbsp;',
  547. $error + ['data' => $field_name],
  548. $error + ['data' => '&lt;unconfigured&gt;'],
  549. $error + ['data' => $field_value],
  550. ];
  551. }
  552. }
  553. $ret = [
  554. '#theme' => 'table',
  555. '#header' => $header,
  556. '#rows' => $rows,
  557. ];
  558. return $ret;
  559. }