1<?php 2 3namespace FPM; 4 5class Status 6{ 7 const HTML_TITLE = 'PHP-FPM Status Page'; 8 9 /** 10 * @var array 11 */ 12 private $contentTypes = [ 13 'plain' => 'text/plain', 14 'html' => 'text/html', 15 'xml' => 'text/xml', 16 'json' => 'application/json', 17 'openmetrics' => 'application/openmetrics-text; version=1.0.0; charset=utf-8', 18 ]; 19 20 /** 21 * @var array 22 */ 23 private $defaultFields = [ 24 'pool' => '\w+', 25 'process manager' => '(static|dynamic|ondemand)', 26 'start time' => '\d+\/\w{3}\/\d{4}:\d{2}:\d{2}:\d{2}\s[+-]\d{4}', 27 'start since' => '\d+', 28 'accepted conn' => '\d+', 29 'listen queue' => '\d+', 30 'max listen queue' => '\d+', 31 'listen queue len' => '\d+', 32 'idle processes' => '\d+', 33 'active processes' => '\d+', 34 'total processes' => '\d+', 35 'max active processes' => '\d+', 36 'max children reached' => '\d+', 37 'slow requests' => '\d+', 38 'memory peak' => '\d+', 39 ]; 40 41 /** 42 * @var Tester 43 */ 44 private Tester $tester; 45 46 /** 47 * @param Tester $tester 48 */ 49 public function __construct(Tester $tester) 50 { 51 $this->tester = $tester; 52 } 53 54 /** 55 * @param string $body 56 * @param string $pattern 57 * @return void 58 */ 59 private function matchError(string $body, string $pattern): void 60 { 61 echo "ERROR: Expected body does not match pattern\n"; 62 echo "BODY:\n"; 63 var_dump($body); 64 echo "PATTERN:\n"; 65 var_dump($pattern); 66 $this->tester->printLogs(); 67 } 68 69 /** 70 * Check status page. 71 * 72 * @param Response $response 73 * @param array $fields 74 * @param string $type 75 * @throws \Exception 76 */ 77 public function checkStatus(Response $response, array $fields, string $type) 78 { 79 if (!isset($this->contentTypes[$type])) { 80 throw new \Exception('Invalid content type ' . $type); 81 } 82 83 $body = $response->getBody($this->contentTypes[$type]); 84 if ($body === null) { 85 return; 86 } 87 $method = "checkStatus" . ucfirst($type); 88 89 $this->$method($body, array_merge($this->defaultFields, $fields)); 90 } 91 92 /** 93 * Make status check for status page. 94 * 95 * @param string $body 96 * @param array $fields 97 * @param string $rowPattern 98 * @param string $header 99 * @param string $footer 100 * @param null|callable $nameTransformer 101 * @param null|callable $valueTransformer 102 * @param bool $startTimeTimestamp 103 * @param bool $closingName 104 */ 105 private function makeStatusCheck( 106 string $body, 107 array $fields, 108 string $rowPattern, 109 string $header = '', 110 string $footer = '', 111 $nameTransformer = null, 112 $valueTransformer = null, 113 bool $startTimeTimestamp = false, 114 bool $closingName = false 115 ) { 116 117 if ($startTimeTimestamp && $fields['start time'][0] === '\\') { 118 $fields['start time'] = '\d+'; 119 } 120 $pattern = '(' . $header; 121 foreach ($fields as $name => $value) { 122 if ($nameTransformer) { 123 $name = call_user_func($nameTransformer, $name); 124 } 125 if ($valueTransformer) { 126 $value = call_user_func($valueTransformer, $value); 127 } 128 if ($closingName) { 129 $pattern .= sprintf($rowPattern, $name, $value, $name); 130 } else { 131 $pattern .= sprintf($rowPattern, $name, $value); 132 } 133 } 134 $pattern = rtrim($pattern, $rowPattern[strlen($rowPattern) - 1]); 135 $pattern .= $footer . ')'; 136 137 if (!preg_match($pattern, $body)) { 138 $this->matchError($body, $pattern); 139 } 140 } 141 142 /** 143 * Check plain status page. 144 * 145 * @param string $body 146 * @param array $fields 147 */ 148 protected function checkStatusPlain(string $body, array $fields) 149 { 150 $this->makeStatusCheck($body, $fields, "%s:\s+%s\n"); 151 } 152 153 /** 154 * Check html status page. 155 * 156 * @param string $body 157 * @param array $fields 158 */ 159 protected function checkStatusHtml(string $body, array $fields) 160 { 161 $header = "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" " . 162 "\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">\n" . 163 "<html xmlns=\"http://www.w3.org/1999/xhtml\" xml:lang=\"en\" lang=\"en\">\n" . 164 "<head><title>" . self::HTML_TITLE . "</title></head>\n" . 165 "<body>\n<table>\n"; 166 $footer = "\n</table>\n</body></html>"; 167 168 $this->makeStatusCheck( 169 $body, 170 $fields, 171 "<tr><th>%s</th><td>%s</td></tr>\n", 172 $header, 173 $footer 174 ); 175 } 176 177 /** 178 * Check xml status page. 179 * 180 * @param string $body 181 * @param array $fields 182 */ 183 protected function checkStatusXml(string $body, array $fields) 184 { 185 $this->makeStatusCheck( 186 $body, 187 $fields, 188 "<%s>%s</%s>\n", 189 "<\?xml version=\"1.0\" \?>\n<status>\n", 190 "\n</status>", 191 function ($name) { 192 return str_replace(' ', '-', $name); 193 }, 194 null, 195 true, 196 true 197 ); 198 } 199 200 /** 201 * Check json status page. 202 * 203 * @param string $body 204 * @param array $fields 205 */ 206 protected function checkStatusJson(string $body, array $fields) 207 { 208 $this->makeStatusCheck( 209 $body, 210 $fields, 211 '"%s":%s,', 212 '{', 213 '}', 214 null, 215 function ($value) { 216 if (is_numeric($value) || $value === '\d+') { 217 return $value; 218 } 219 220 return '"' . $value . '"'; 221 }, 222 true 223 ); 224 } 225 226 /** 227 * Check openmetrics status page. 228 * 229 * @param string $body 230 * @param array $fields 231 */ 232 protected function checkStatusOpenmetrics(string $body, array $fields) 233 { 234 $pattern = "(# HELP phpfpm_up Could pool " . $fields['pool'] . " using a " . $fields['process manager'] . " PM on PHP-FPM be reached\?\n" . 235 "# TYPE phpfpm_up gauge\n" . 236 "phpfpm_up 1\n" . 237 "# HELP phpfpm_start_since The number of seconds since FPM has started\.\n" . 238 "# TYPE phpfpm_start_since counter\n" . 239 "phpfpm_start_since " . $fields['start since'] . "\n" . 240 "# HELP phpfpm_accepted_connections The number of requests accepted by the pool\.\n" . 241 "# TYPE phpfpm_accepted_connections counter\n" . 242 "phpfpm_accepted_connections " . $fields['accepted conn'] . "\n" . 243 "# HELP phpfpm_listen_queue The number of requests in the queue of pending connections\.\n" . 244 "# TYPE phpfpm_listen_queue gauge\n" . 245 "phpfpm_listen_queue " . $fields['listen queue'] . "\n" . 246 "# HELP phpfpm_max_listen_queue The maximum number of requests in the queue of pending connections since FPM has started\.\n" . 247 "# TYPE phpfpm_max_listen_queue counter\n" . 248 "phpfpm_max_listen_queue " . $fields['max listen queue'] . "\n" . 249 "# TYPE phpfpm_listen_queue_length gauge\n" . 250 "# HELP phpfpm_listen_queue_length The size of the socket queue of pending connections\.\n" . 251 "phpfpm_listen_queue_length " . $fields['listen queue len'] . "\n" . 252 "# HELP phpfpm_idle_processes The number of idle processes\.\n" . 253 "# TYPE phpfpm_idle_processes gauge\n" . 254 "phpfpm_idle_processes " . $fields['idle processes'] . "\n" . 255 "# HELP phpfpm_active_processes The number of active processes\.\n" . 256 "# TYPE phpfpm_active_processes gauge\n" . 257 "phpfpm_active_processes " . $fields['active processes'] . "\n" . 258 "# HELP phpfpm_total_processes The number of idle \+ active processes\.\n" . 259 "# TYPE phpfpm_total_processes gauge\n" . 260 "phpfpm_total_processes " . $fields['total processes'] . "\n" . 261 "# HELP phpfpm_max_active_processes The maximum number of active processes since FPM has started\.\n" . 262 "# TYPE phpfpm_max_active_processes counter\n" . 263 "phpfpm_max_active_processes " . $fields['max active processes'] . "\n" . 264 "# HELP phpfpm_max_children_reached The number of times, the process limit has been reached, when pm tries to start more children \(works only for pm 'dynamic' and 'ondemand'\)\.\n" . 265 "# TYPE phpfpm_max_children_reached counter\n" . 266 "phpfpm_max_children_reached " . $fields['max children reached'] . "\n" . 267 "# HELP phpfpm_slow_requests The number of requests that exceeded your 'request_slowlog_timeout' value\.\n" . 268 "# TYPE phpfpm_slow_requests counter\n" . 269 "phpfpm_slow_requests " . $fields['slow requests'] . "\n" . 270 "# HELP phpfpm_memory_peak The memory usage peak since FPM has started\.\n" . 271 "# TYPE phpfpm_memory_peak gauge\n" . 272 "phpfpm_memory_peak " . $fields['memory peak'] . "\n" . 273 "# EOF)\n"; 274 275 if (!preg_match($pattern, $body)) { 276 $this->matchError($body, $pattern); 277 } 278 } 279} 280