Monitor.php 17 KB

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