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