PageRenderTime 48ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 0ms

/libraries/classes/Server/Status/Monitor.php

http://github.com/phpmyadmin/phpmyadmin
PHP | 555 lines | 362 code | 78 blank | 115 comment | 33 complexity | 2c0a8ee4e663395467b88199136429ed MD5 | raw file
Possible License(s): GPL-2.0, MIT, LGPL-3.0
  1. <?php
  2. /**
  3. * functions for displaying server status sub item: monitor
  4. */
  5. declare(strict_types=1);
  6. namespace PhpMyAdmin\Server\Status;
  7. use PhpMyAdmin\DatabaseInterface;
  8. use PhpMyAdmin\Profiling;
  9. use PhpMyAdmin\Server\SysInfo\SysInfo;
  10. use PhpMyAdmin\Util;
  11. use function array_sum;
  12. use function count;
  13. use function implode;
  14. use function is_numeric;
  15. use function json_decode;
  16. use function mb_strlen;
  17. use function mb_strpos;
  18. use function mb_strtolower;
  19. use function mb_substr;
  20. use function microtime;
  21. use function preg_match;
  22. use function preg_replace;
  23. use function strlen;
  24. /**
  25. * functions for displaying server status sub item: monitor
  26. */
  27. class Monitor
  28. {
  29. /** @var DatabaseInterface */
  30. private $dbi;
  31. /**
  32. * @param DatabaseInterface $dbi DatabaseInterface instance
  33. */
  34. public function __construct($dbi)
  35. {
  36. $this->dbi = $dbi;
  37. }
  38. /**
  39. * Returns JSON for real-time charting data
  40. *
  41. * @param string $requiredData Required data
  42. *
  43. * @return array JSON
  44. */
  45. public function getJsonForChartingData(string $requiredData): array
  46. {
  47. $ret = json_decode($requiredData, true);
  48. $statusVars = [];
  49. $serverVars = [];
  50. $sysinfo = $cpuload = $memory = 0;
  51. /* Accumulate all required variables and data */
  52. [$serverVars, $statusVars, $ret] = $this->getJsonForChartingDataGet(
  53. $ret,
  54. $serverVars,
  55. $statusVars,
  56. $sysinfo,
  57. $cpuload,
  58. $memory
  59. );
  60. // Retrieve all required status variables
  61. $statusVarValues = [];
  62. if (count($statusVars)) {
  63. $statusVarValues = $this->dbi->fetchResult(
  64. "SHOW GLOBAL STATUS WHERE Variable_name='"
  65. . implode("' OR Variable_name='", $statusVars) . "'",
  66. 0,
  67. 1
  68. );
  69. }
  70. // Retrieve all required server variables
  71. $serverVarValues = [];
  72. if (count($serverVars)) {
  73. $serverVarValues = $this->dbi->fetchResult(
  74. "SHOW GLOBAL VARIABLES WHERE Variable_name='"
  75. . implode("' OR Variable_name='", $serverVars) . "'",
  76. 0,
  77. 1
  78. );
  79. }
  80. // ...and now assign them
  81. $ret = $this->getJsonForChartingDataSet($ret, $statusVarValues, $serverVarValues);
  82. $ret['x'] = (int) (microtime(true) * 1000);
  83. return $ret;
  84. }
  85. /**
  86. * Assign the variables for real-time charting data
  87. *
  88. * @param array $ret Real-time charting data
  89. * @param array $statusVarValues Status variable values
  90. * @param array $serverVarValues Server variable values
  91. *
  92. * @return array
  93. */
  94. private function getJsonForChartingDataSet(
  95. array $ret,
  96. array $statusVarValues,
  97. array $serverVarValues
  98. ): array {
  99. foreach ($ret as $chart_id => $chartNodes) {
  100. foreach ($chartNodes as $node_id => $nodeDataPoints) {
  101. foreach ($nodeDataPoints as $point_id => $dataPoint) {
  102. switch ($dataPoint['type']) {
  103. case 'statusvar':
  104. $ret[$chart_id][$node_id][$point_id]['value'] = $statusVarValues[$dataPoint['name']];
  105. break;
  106. case 'servervar':
  107. $ret[$chart_id][$node_id][$point_id]['value'] = $serverVarValues[$dataPoint['name']];
  108. break;
  109. }
  110. }
  111. }
  112. }
  113. return $ret;
  114. }
  115. /**
  116. * Get called to get JSON for charting data
  117. *
  118. * @param array $ret Real-time charting data
  119. * @param array $serverVars Server variable values
  120. * @param array $statusVars Status variable values
  121. * @param mixed $sysinfo System info
  122. * @param mixed $cpuload CPU load
  123. * @param mixed $memory Memory
  124. *
  125. * @return array
  126. */
  127. private function getJsonForChartingDataGet(
  128. array $ret,
  129. array $serverVars,
  130. array $statusVars,
  131. $sysinfo,
  132. $cpuload,
  133. $memory
  134. ) {
  135. // For each chart
  136. foreach ($ret as $chartId => $chartNodes) {
  137. // For each data series
  138. foreach ($chartNodes as $nodeId => $nodeDataPoints) {
  139. // For each data point in the series (usually just 1)
  140. foreach ($nodeDataPoints as $pointId => $dataPoint) {
  141. [$serverVars, $statusVars, $ret[$chartId][$nodeId][$pointId]] = $this->getJsonForChartingDataSwitch(
  142. $dataPoint['type'],
  143. $dataPoint['name'],
  144. $serverVars,
  145. $statusVars,
  146. $ret[$chartId][$nodeId][$pointId],
  147. $sysinfo,
  148. $cpuload,
  149. $memory
  150. );
  151. } /* foreach */
  152. } /* foreach */
  153. }
  154. return [
  155. $serverVars,
  156. $statusVars,
  157. $ret,
  158. ];
  159. }
  160. /**
  161. * Switch called to get JSON for charting data
  162. *
  163. * @param string $type Type
  164. * @param string $pName Name
  165. * @param array $serverVars Server variable values
  166. * @param array $statusVars Status variable values
  167. * @param array $ret Real-time charting data
  168. * @param mixed $sysinfo System info
  169. * @param mixed $cpuload CPU load
  170. * @param mixed $memory Memory
  171. *
  172. * @return array
  173. */
  174. private function getJsonForChartingDataSwitch(
  175. $type,
  176. $pName,
  177. array $serverVars,
  178. array $statusVars,
  179. array $ret,
  180. $sysinfo,
  181. $cpuload,
  182. $memory
  183. ) {
  184. /**
  185. * We only collect the status and server variables here to read them all in one query,
  186. * and only afterwards assign them. Also do some allow list filtering on the names
  187. */
  188. switch ($type) {
  189. case 'servervar':
  190. if (! preg_match('/[^a-zA-Z_]+/', $pName)) {
  191. $serverVars[] = $pName;
  192. }
  193. break;
  194. case 'statusvar':
  195. if (! preg_match('/[^a-zA-Z_]+/', $pName)) {
  196. $statusVars[] = $pName;
  197. }
  198. break;
  199. case 'proc':
  200. $result = $this->dbi->query('SHOW PROCESSLIST');
  201. $ret['value'] = $this->dbi->numRows($result);
  202. break;
  203. case 'cpu':
  204. if (! $sysinfo) {
  205. $sysinfo = SysInfo::get();
  206. }
  207. if (! $cpuload) {
  208. $cpuload = $sysinfo->loadavg();
  209. }
  210. if (SysInfo::getOs() === 'Linux') {
  211. $ret['idle'] = $cpuload['idle'];
  212. $ret['busy'] = $cpuload['busy'];
  213. } else {
  214. $ret['value'] = $cpuload['loadavg'];
  215. }
  216. break;
  217. case 'memory':
  218. if (! $sysinfo) {
  219. $sysinfo = SysInfo::get();
  220. }
  221. if (! $memory) {
  222. $memory = $sysinfo->memory();
  223. }
  224. $ret['value'] = $memory[$pName] ?? 0;
  225. break;
  226. }
  227. return [
  228. $serverVars,
  229. $statusVars,
  230. $ret,
  231. ];
  232. }
  233. /**
  234. * Returns JSON for log data with type: slow
  235. *
  236. * @param int $start Unix Time: Start time for query
  237. * @param int $end Unix Time: End time for query
  238. *
  239. * @return array
  240. */
  241. public function getJsonForLogDataTypeSlow(int $start, int $end): array
  242. {
  243. $query = 'SELECT start_time, user_host, ';
  244. $query .= 'Sec_to_Time(Sum(Time_to_Sec(query_time))) as query_time, ';
  245. $query .= 'Sec_to_Time(Sum(Time_to_Sec(lock_time))) as lock_time, ';
  246. $query .= 'SUM(rows_sent) AS rows_sent, ';
  247. $query .= 'SUM(rows_examined) AS rows_examined, db, sql_text, ';
  248. $query .= 'COUNT(sql_text) AS \'#\' ';
  249. $query .= 'FROM `mysql`.`slow_log` ';
  250. $query .= 'WHERE start_time > FROM_UNIXTIME(' . $start . ') ';
  251. $query .= 'AND start_time < FROM_UNIXTIME(' . $end . ') GROUP BY sql_text';
  252. $result = $this->dbi->tryQuery($query);
  253. $return = [
  254. 'rows' => [],
  255. 'sum' => [],
  256. ];
  257. while ($row = $this->dbi->fetchAssoc($result)) {
  258. $type = mb_strtolower(
  259. mb_substr(
  260. $row['sql_text'],
  261. 0,
  262. (int) mb_strpos($row['sql_text'], ' ')
  263. )
  264. );
  265. switch ($type) {
  266. case 'insert':
  267. case 'update':
  268. //Cut off big inserts and updates, but append byte count instead
  269. if (mb_strlen($row['sql_text']) > 220) {
  270. $implodeSqlText = implode(
  271. ' ',
  272. (array) Util::formatByteDown(
  273. mb_strlen($row['sql_text']),
  274. 2,
  275. 2
  276. )
  277. );
  278. $row['sql_text'] = mb_substr($row['sql_text'], 0, 200)
  279. . '... [' . $implodeSqlText . ']';
  280. }
  281. break;
  282. default:
  283. break;
  284. }
  285. if (! isset($return['sum'][$type])) {
  286. $return['sum'][$type] = 0;
  287. }
  288. $return['sum'][$type] += $row['#'];
  289. $return['rows'][] = $row;
  290. }
  291. $return['sum']['TOTAL'] = array_sum($return['sum']);
  292. $return['numRows'] = count($return['rows']);
  293. $this->dbi->freeResult($result);
  294. return $return;
  295. }
  296. /**
  297. * Returns JSon for log data with type: general
  298. *
  299. * @param int $start Unix Time: Start time for query
  300. * @param int $end Unix Time: End time for query
  301. * @param bool $isTypesLimited Whether to limit types or not
  302. * @param bool $removeVariables Whether to remove variables or not
  303. *
  304. * @return array
  305. */
  306. public function getJsonForLogDataTypeGeneral(
  307. int $start,
  308. int $end,
  309. bool $isTypesLimited,
  310. bool $removeVariables
  311. ): array {
  312. $limitTypes = '';
  313. if ($isTypesLimited) {
  314. $limitTypes = 'AND argument REGEXP \'^(INSERT|SELECT|UPDATE|DELETE)\' ';
  315. }
  316. $query = 'SELECT TIME(event_time) as event_time, user_host, thread_id, ';
  317. $query .= 'server_id, argument, count(argument) as \'#\' ';
  318. $query .= 'FROM `mysql`.`general_log` ';
  319. $query .= 'WHERE command_type=\'Query\' ';
  320. $query .= 'AND event_time > FROM_UNIXTIME(' . $start . ') ';
  321. $query .= 'AND event_time < FROM_UNIXTIME(' . $end . ') ';
  322. $query .= $limitTypes . 'GROUP by argument'; // HAVING count > 1';
  323. $result = $this->dbi->tryQuery($query);
  324. $return = [
  325. 'rows' => [],
  326. 'sum' => [],
  327. ];
  328. $insertTables = [];
  329. $insertTablesFirst = -1;
  330. $i = 0;
  331. while ($row = $this->dbi->fetchAssoc($result)) {
  332. preg_match('/^(\w+)\s/', $row['argument'], $match);
  333. $type = mb_strtolower($match[1]);
  334. if (! isset($return['sum'][$type])) {
  335. $return['sum'][$type] = 0;
  336. }
  337. $return['sum'][$type] += $row['#'];
  338. switch ($type) {
  339. /** @noinspection PhpMissingBreakStatementInspection */
  340. case 'insert':
  341. // Group inserts if selected
  342. if (
  343. $removeVariables && preg_match(
  344. '/^INSERT INTO (`|\'|"|)([^\s\\1]+)\\1/i',
  345. $row['argument'],
  346. $matches
  347. )
  348. ) {
  349. $insertTables[$matches[2]]++;
  350. if ($insertTables[$matches[2]] > 1) {
  351. $return['rows'][$insertTablesFirst]['#'] = $insertTables[$matches[2]];
  352. // Add a ... to the end of this query to indicate that
  353. // there's been other queries
  354. $temp = $return['rows'][$insertTablesFirst]['argument'];
  355. $return['rows'][$insertTablesFirst]['argument'] .= $this->getSuspensionPoints(
  356. $temp[strlen($temp) - 1]
  357. );
  358. // Group this value, thus do not add to the result list
  359. continue 2;
  360. }
  361. $insertTablesFirst = $i;
  362. $insertTables[$matches[2]] += $row['#'] - 1;
  363. }
  364. // No break here
  365. case 'update':
  366. // Cut off big inserts and updates,
  367. // but append byte count therefor
  368. if (mb_strlen($row['argument']) > 220) {
  369. $row['argument'] = mb_substr($row['argument'], 0, 200)
  370. . '... ['
  371. . implode(
  372. ' ',
  373. (array) Util::formatByteDown(
  374. mb_strlen($row['argument']),
  375. 2,
  376. 2
  377. )
  378. )
  379. . ']';
  380. }
  381. break;
  382. default:
  383. break;
  384. }
  385. $return['rows'][] = $row;
  386. $i++;
  387. }
  388. $return['sum']['TOTAL'] = array_sum($return['sum']);
  389. $return['numRows'] = count($return['rows']);
  390. $this->dbi->freeResult($result);
  391. return $return;
  392. }
  393. /**
  394. * Return suspension points if needed
  395. *
  396. * @param string $lastChar Last char
  397. *
  398. * @return string Return suspension points if needed
  399. */
  400. private function getSuspensionPoints(string $lastChar): string
  401. {
  402. if ($lastChar !== '.') {
  403. return '<br>...';
  404. }
  405. return '';
  406. }
  407. /**
  408. * Returns JSON for logging vars
  409. *
  410. * @param string|null $name Variable name
  411. * @param string|null $value Variable value
  412. *
  413. * @return array JSON
  414. */
  415. public function getJsonForLoggingVars(?string $name, ?string $value): array
  416. {
  417. if (isset($name, $value)) {
  418. $escapedValue = $this->dbi->escapeString($value);
  419. if (! is_numeric($escapedValue)) {
  420. $escapedValue = "'" . $escapedValue . "'";
  421. }
  422. if (! preg_match('/[^a-zA-Z0-9_]+/', $name)) {
  423. $this->dbi->query('SET GLOBAL ' . $name . ' = ' . $escapedValue);
  424. }
  425. }
  426. return $this->dbi->fetchResult(
  427. 'SHOW GLOBAL VARIABLES WHERE Variable_name IN'
  428. . ' ("general_log","slow_query_log","long_query_time","log_output")',
  429. 0,
  430. 1
  431. );
  432. }
  433. /**
  434. * Returns JSON for query_analyzer
  435. *
  436. * @param string $database Database name
  437. * @param string $query SQL query
  438. *
  439. * @return array JSON
  440. */
  441. public function getJsonForQueryAnalyzer(
  442. string $database,
  443. string $query
  444. ): array {
  445. global $cached_affected_rows;
  446. $return = [];
  447. if (strlen($database) > 0) {
  448. $this->dbi->selectDb($database);
  449. }
  450. $profiling = Profiling::isSupported($this->dbi);
  451. if ($profiling) {
  452. $this->dbi->query('SET PROFILING=1;');
  453. }
  454. // Do not cache query
  455. $sqlQuery = preg_replace('/^(\s*SELECT)/i', '\\1 SQL_NO_CACHE', $query);
  456. $this->dbi->tryQuery($sqlQuery);
  457. $return['affectedRows'] = $cached_affected_rows;
  458. $result = $this->dbi->tryQuery('EXPLAIN ' . $sqlQuery);
  459. while ($row = $this->dbi->fetchAssoc($result)) {
  460. $return['explain'][] = $row;
  461. }
  462. // In case an error happened
  463. $return['error'] = $this->dbi->getError();
  464. $this->dbi->freeResult($result);
  465. if ($profiling) {
  466. $return['profiling'] = [];
  467. $result = $this->dbi->tryQuery(
  468. 'SELECT seq,state,duration FROM INFORMATION_SCHEMA.PROFILING WHERE QUERY_ID=1 ORDER BY seq'
  469. );
  470. while ($row = $this->dbi->fetchAssoc($result)) {
  471. $return['profiling'][] = $row;
  472. }
  473. $this->dbi->freeResult($result);
  474. }
  475. return $return;
  476. }
  477. }