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