Monitor.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546
  1. <?php
  2. /* vim: set expandtab sw=4 ts=4 sts=4: */
  3. /**
  4. * functions for displaying server status sub item: monitor
  5. *
  6. * @usedby server_status_monitor.php
  7. *
  8. * @package PhpMyAdmin
  9. */
  10. declare(strict_types=1);
  11. namespace PhpMyAdmin\Server\Status;
  12. use PhpMyAdmin\DatabaseInterface;
  13. use PhpMyAdmin\SysInfo;
  14. use PhpMyAdmin\Util;
  15. /**
  16. * functions for displaying server status sub item: monitor
  17. *
  18. * @package PhpMyAdmin
  19. */
  20. class Monitor
  21. {
  22. /**
  23. * @var DatabaseInterface
  24. */
  25. private $dbi;
  26. /**
  27. * Monitor constructor.
  28. * @param DatabaseInterface $dbi DatabaseInterface instance
  29. */
  30. public function __construct($dbi)
  31. {
  32. $this->dbi = $dbi;
  33. }
  34. /**
  35. * Returns JSON for real-time charting data
  36. *
  37. * @param string $requiredData Required data
  38. *
  39. * @return array JSON
  40. */
  41. public function getJsonForChartingData(string $requiredData): array
  42. {
  43. $ret = json_decode($requiredData, true);
  44. $statusVars = [];
  45. $serverVars = [];
  46. $sysinfo = $cpuload = $memory = 0;
  47. /* Accumulate all required variables and data */
  48. list($serverVars, $statusVars, $ret) = $this->getJsonForChartingDataGet(
  49. $ret,
  50. $serverVars,
  51. $statusVars,
  52. $sysinfo,
  53. $cpuload,
  54. $memory
  55. );
  56. // Retrieve all required status variables
  57. $statusVarValues = [];
  58. if (count($statusVars)) {
  59. $statusVarValues = $this->dbi->fetchResult(
  60. "SHOW GLOBAL STATUS WHERE Variable_name='"
  61. . implode("' OR Variable_name='", $statusVars) . "'",
  62. 0,
  63. 1
  64. );
  65. }
  66. // Retrieve all required server variables
  67. $serverVarValues = [];
  68. if (count($serverVars)) {
  69. $serverVarValues = $this->dbi->fetchResult(
  70. "SHOW GLOBAL VARIABLES WHERE Variable_name='"
  71. . implode("' OR Variable_name='", $serverVars) . "'",
  72. 0,
  73. 1
  74. );
  75. }
  76. // ...and now assign them
  77. $ret = $this->getJsonForChartingDataSet($ret, $statusVarValues, $serverVarValues);
  78. $ret['x'] = microtime(true) * 1000;
  79. return $ret;
  80. }
  81. /**
  82. * Assign the variables for real-time charting data
  83. *
  84. * @param array $ret Real-time charting data
  85. * @param array $statusVarValues Status variable values
  86. * @param array $serverVarValues Server variable values
  87. *
  88. * @return array
  89. */
  90. private function getJsonForChartingDataSet(
  91. array $ret,
  92. array $statusVarValues,
  93. array $serverVarValues
  94. ): array {
  95. foreach ($ret as $chart_id => $chartNodes) {
  96. foreach ($chartNodes as $node_id => $nodeDataPoints) {
  97. foreach ($nodeDataPoints as $point_id => $dataPoint) {
  98. switch ($dataPoint['type']) {
  99. case 'statusvar':
  100. $ret[$chart_id][$node_id][$point_id]['value']
  101. = $statusVarValues[$dataPoint['name']];
  102. break;
  103. case 'servervar':
  104. $ret[$chart_id][$node_id][$point_id]['value']
  105. = $serverVarValues[$dataPoint['name']];
  106. break;
  107. }
  108. }
  109. }
  110. }
  111. return $ret;
  112. }
  113. /**
  114. * Get called to get JSON for charting data
  115. *
  116. * @param array $ret Real-time charting data
  117. * @param array $serverVars Server variable values
  118. * @param array $statusVars Status variable values
  119. * @param mixed $sysinfo System info
  120. * @param mixed $cpuload CPU load
  121. * @param mixed $memory Memory
  122. *
  123. * @return array
  124. */
  125. private function getJsonForChartingDataGet(
  126. array $ret,
  127. array $serverVars,
  128. array $statusVars,
  129. $sysinfo,
  130. $cpuload,
  131. $memory
  132. ) {
  133. // For each chart
  134. foreach ($ret as $chartId => $chartNodes) {
  135. // For each data series
  136. foreach ($chartNodes as $nodeId => $nodeDataPoints) {
  137. // For each data point in the series (usually just 1)
  138. foreach ($nodeDataPoints as $pointId => $dataPoint) {
  139. list($serverVars, $statusVars, $ret[$chartId][$nodeId][$pointId])
  140. = $this->getJsonForChartingDataSwitch(
  141. $dataPoint['type'],
  142. $dataPoint['name'],
  143. $serverVars,
  144. $statusVars,
  145. $ret[$chartId][$nodeId][$pointId],
  146. $sysinfo,
  147. $cpuload,
  148. $memory
  149. );
  150. } /* foreach */
  151. } /* foreach */
  152. }
  153. return [
  154. $serverVars,
  155. $statusVars,
  156. $ret,
  157. ];
  158. }
  159. /**
  160. * Switch called to get JSON for charting data
  161. *
  162. * @param string $type Type
  163. * @param string $pName Name
  164. * @param array $serverVars Server variable values
  165. * @param array $statusVars Status variable values
  166. * @param array $ret Real-time charting data
  167. * @param mixed $sysinfo System info
  168. * @param mixed $cpuload CPU load
  169. * @param mixed $memory Memory
  170. *
  171. * @return array
  172. */
  173. private function getJsonForChartingDataSwitch(
  174. $type,
  175. $pName,
  176. array $serverVars,
  177. array $statusVars,
  178. array $ret,
  179. $sysinfo,
  180. $cpuload,
  181. $memory
  182. ) {
  183. switch ($type) {
  184. /* We only collect the status and server variables here to
  185. * read them all in one query,
  186. * and only afterwards assign them.
  187. * Also do some white list filtering on the names
  188. */
  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'] = isset($memory[$pName]) ? $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. 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. 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 ($removeVariables
  343. && 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]['#']
  352. = $insertTables[$matches[2]];
  353. // Add a ... to the end of this query to indicate that
  354. // there's been other queries
  355. $temp = $return['rows'][$insertTablesFirst]['argument'];
  356. $return['rows'][$insertTablesFirst]['argument']
  357. .= $this->getSuspensionPoints(
  358. $temp[strlen($temp) - 1]
  359. );
  360. // Group this value, thus do not add to the result list
  361. continue 2;
  362. } else {
  363. $insertTablesFirst = $i;
  364. $insertTables[$matches[2]] += $row['#'] - 1;
  365. }
  366. }
  367. // No break here
  368. case 'update':
  369. // Cut off big inserts and updates,
  370. // but append byte count therefor
  371. if (mb_strlen($row['argument']) > 220) {
  372. $row['argument'] = mb_substr($row['argument'], 0, 200)
  373. . '... ['
  374. . implode(
  375. ' ',
  376. Util::formatByteDown(
  377. mb_strlen($row['argument']),
  378. 2,
  379. 2
  380. )
  381. )
  382. . ']';
  383. }
  384. break;
  385. default:
  386. break;
  387. }
  388. $return['rows'][] = $row;
  389. $i++;
  390. }
  391. $return['sum']['TOTAL'] = array_sum($return['sum']);
  392. $return['numRows'] = count($return['rows']);
  393. $this->dbi->freeResult($result);
  394. return $return;
  395. }
  396. /**
  397. * Return suspension points if needed
  398. *
  399. * @param string $lastChar Last char
  400. *
  401. * @return string Return suspension points if needed
  402. */
  403. private function getSuspensionPoints(string $lastChar): string
  404. {
  405. if ($lastChar != '.') {
  406. return '<br>...';
  407. }
  408. return '';
  409. }
  410. /**
  411. * Returns JSON for logging vars
  412. *
  413. * @param string|null $name Variable name
  414. * @param string|null $value Variable value
  415. *
  416. * @return array JSON
  417. */
  418. public function getJsonForLoggingVars(?string $name, ?string $value): array
  419. {
  420. if (isset($name) && isset($value)) {
  421. $escapedValue = $this->dbi->escapeString($value);
  422. if (! is_numeric($escapedValue)) {
  423. $escapedValue = "'" . $escapedValue . "'";
  424. }
  425. if (! preg_match("/[^a-zA-Z0-9_]+/", $name)) {
  426. $this->dbi->query(
  427. 'SET GLOBAL ' . $name . ' = ' . $escapedValue
  428. );
  429. }
  430. }
  431. $loggingVars = $this->dbi->fetchResult(
  432. 'SHOW GLOBAL VARIABLES WHERE Variable_name IN'
  433. . ' ("general_log","slow_query_log","long_query_time","log_output")',
  434. 0,
  435. 1
  436. );
  437. return $loggingVars;
  438. }
  439. /**
  440. * Returns JSON for query_analyzer
  441. *
  442. * @param string $database Database name
  443. * @param string $query SQL query
  444. *
  445. * @return array JSON
  446. */
  447. public function getJsonForQueryAnalyzer(
  448. string $database,
  449. string $query
  450. ): array {
  451. global $cached_affected_rows;
  452. $return = [];
  453. if (strlen($database) > 0) {
  454. $this->dbi->selectDb($database);
  455. }
  456. if ($profiling = Util::profilingSupported()) {
  457. $this->dbi->query('SET PROFILING=1;');
  458. }
  459. // Do not cache query
  460. $sqlQuery = preg_replace(
  461. '/^(\s*SELECT)/i',
  462. '\\1 SQL_NO_CACHE',
  463. $query
  464. );
  465. $this->dbi->tryQuery($sqlQuery);
  466. $return['affectedRows'] = $cached_affected_rows;
  467. $result = $this->dbi->tryQuery('EXPLAIN ' . $sqlQuery);
  468. while ($row = $this->dbi->fetchAssoc($result)) {
  469. $return['explain'][] = $row;
  470. }
  471. // In case an error happened
  472. $return['error'] = $this->dbi->getError();
  473. $this->dbi->freeResult($result);
  474. if ($profiling) {
  475. $return['profiling'] = [];
  476. $result = $this->dbi->tryQuery(
  477. 'SELECT seq,state,duration FROM INFORMATION_SCHEMA.PROFILING'
  478. . ' WHERE QUERY_ID=1 ORDER BY seq'
  479. );
  480. while ($row = $this->dbi->fetchAssoc($result)) {
  481. $return['profiling'][] = $row;
  482. }
  483. $this->dbi->freeResult($result);
  484. }
  485. return $return;
  486. }
  487. }