1<?php 2 3namespace FPM; 4 5class LogReader 6{ 7 /** 8 * Log debugging. 9 * 10 * @var bool 11 */ 12 private bool $debug; 13 14 /** 15 * Log descriptor. 16 * 17 * @var string|null 18 */ 19 private ?string $currentSourceName; 20 21 /** 22 * Log descriptors. 23 * 24 * @var LogSource[] 25 */ 26 private array $sources = []; 27 28 /** 29 * Log reader constructor. 30 * 31 * @param bool $debug 32 */ 33 public function __construct(bool $debug = false) 34 { 35 $this->debug = $debug; 36 } 37 38 /** 39 * Returns log descriptor source. 40 * 41 * @return LogSource 42 * @throws \Exception 43 */ 44 private function getSource(): LogSource 45 { 46 if ( ! $this->currentSourceName) { 47 throw new \Exception('Log descriptor is not set'); 48 } 49 50 return $this->sources[$this->currentSourceName]; 51 } 52 53 /** 54 * Set current stream source and create it if it does not exist. 55 * 56 * @param string $name Stream name. 57 * @param resource $stream The actual stream. 58 */ 59 public function setStreamSource(string $name, $stream) 60 { 61 $this->currentSourceName = $name; 62 if ( ! isset($this->sources[$name])) { 63 $this->sources[$name] = new LogStreamSource($stream); 64 } 65 } 66 67 /** 68 * Set file source as current and create it if it does not exist. 69 * 70 * @param string $name Source name. 71 * @param string $filePath Source file path.s 72 */ 73 public function setFileSource(string $name, string $filePath) 74 { 75 $this->currentSourceName = $name; 76 if ( ! isset($this->sources[$name])) { 77 $this->sources[$name] = new LogFileSource($filePath); 78 } 79 } 80 81 /** 82 * Get a single log line. 83 * 84 * @param int $timeoutSeconds Read timeout in seconds 85 * @param int $timeoutMicroseconds Read timeout in microseconds 86 * @param bool $throwOnTimeout Whether to throw an exception on timeout 87 * 88 * @return null|string 89 * @throws \Exception 90 */ 91 public function getLine( 92 int $timeoutSeconds = 3, 93 int $timeoutMicroseconds = 0, 94 bool $throwOnTimeout = false 95 ): ?string { 96 $line = $this->getSource()->getLine( 97 $timeoutSeconds, 98 $timeoutMicroseconds, 99 $throwOnTimeout 100 ); 101 $this->trace(is_null($line) ? "LINE - null" : "LINE: $line"); 102 103 return $line; 104 } 105 106 /** 107 * Print separation line. 108 */ 109 public function printSeparator(): void 110 { 111 echo str_repeat('-', 68) . "\n"; 112 } 113 114 /** 115 * Print all logs. 116 */ 117 public function printLogs(): void 118 { 119 $hasMultipleDescriptors = count($this->sources) > 1; 120 echo "LOGS:\n"; 121 foreach ($this->sources as $name => $source) { 122 if ($hasMultipleDescriptors) { 123 echo ">>> source: $name\n"; 124 } 125 $this->printSeparator(); 126 foreach ($source->getAllLines() as $line) { 127 echo $line; 128 } 129 $this->printSeparator(); 130 } 131 } 132 133 /** 134 * Print error and logs. 135 * 136 * @param string|null $errorMessage Error message to print before the logs. 137 * 138 * @return false 139 */ 140 public function printError(?string $errorMessage): bool 141 { 142 if (is_null($errorMessage)) { 143 return false; 144 } 145 echo "ERROR: " . $errorMessage . "\n\n"; 146 $this->printLogs(); 147 echo "\n"; 148 149 return false; 150 } 151 152 /** 153 * Read log until matcher matches the log message or there are no more logs. 154 * 155 * @param callable $matcher Callback to identify a match 156 * @param string|null $notFoundMessage Error message if matcher does not succeed. 157 * @param bool $checkAllLogs Whether to also check past logs. 158 * @param int|null $timeoutSeconds Timeout in seconds for reading of all messages. 159 * @param int|null $timeoutMicroseconds Additional timeout in microseconds for reading of all messages. 160 * 161 * @return bool 162 * @throws \Exception 163 */ 164 public function readUntil( 165 callable $matcher, 166 string $notFoundMessage = null, 167 bool $checkAllLogs = false, 168 int $timeoutSeconds = null, 169 int $timeoutMicroseconds = null 170 ): bool { 171 if (is_null($timeoutSeconds) && is_null($timeoutMicroseconds)) { 172 $timeoutSeconds = 3; 173 $timeoutMicroseconds = 0; 174 } elseif (is_null($timeoutSeconds)) { 175 $timeoutSeconds = 0; 176 } elseif (is_null($timeoutMicroseconds)) { 177 $timeoutMicroseconds = 0; 178 } 179 180 $startTime = microtime(true); 181 $endTime = $startTime + $timeoutSeconds + ($timeoutMicroseconds / 1_000_000); 182 if ($checkAllLogs) { 183 foreach ($this->getSource()->getAllLines() as $line) { 184 if ($matcher($line)) { 185 return true; 186 } 187 } 188 } 189 190 do { 191 if (microtime(true) > $endTime) { 192 return $this->printError($notFoundMessage); 193 } 194 $line = $this->getLine($timeoutSeconds, $timeoutMicroseconds); 195 if ($line === null || microtime(true) > $endTime) { 196 return $this->printError($notFoundMessage); 197 } 198 } while ( ! $matcher($line)); 199 200 return true; 201 } 202 203 /** 204 * Print tracing message - only in debug . 205 * 206 * @param string $msg Message to print. 207 */ 208 private function trace(string $msg): void 209 { 210 if ($this->debug) { 211 print "LogReader - $msg"; 212 } 213 } 214} 215 216class LogTimoutException extends \Exception 217{ 218} 219 220abstract class LogSource 221{ 222 /** 223 * Get single line from the source. 224 * 225 * @param int $timeoutSeconds Read timeout in seconds 226 * @param int $timeoutMicroseconds Read timeout in microseconds 227 * @param bool $throwOnTimeout Whether to throw an exception on timeout 228 * 229 * @return string|null 230 * @throws LogTimoutException 231 */ 232 public abstract function getLine( 233 int $timeoutSeconds, 234 int $timeoutMicroseconds, 235 bool $throwOnTimeout = false 236 ): ?string; 237 238 /** 239 * Get all lines that has been returned by getLine() method. 240 * 241 * @return string[] 242 */ 243 public abstract function getAllLines(): array; 244} 245 246class LogStreamSource extends LogSource 247{ 248 /** 249 * @var resource 250 */ 251 private $stream; 252 253 /** 254 * @var array 255 */ 256 private array $lines = []; 257 258 public function __construct($stream) 259 { 260 $this->stream = $stream; 261 } 262 263 /** 264 * Get single line from the stream. 265 * 266 * @param int $timeoutSeconds Read timeout in seconds 267 * @param int $timeoutMicroseconds Read timeout in microseconds 268 * @param bool $throwOnTimeout Whether to throw an exception on timeout 269 * 270 * @return string|null 271 * @throws LogTimoutException 272 */ 273 public function getLine( 274 int $timeoutSeconds, 275 int $timeoutMicroseconds, 276 bool $throwOnTimeout = false 277 ): ?string { 278 if (feof($this->stream)) { 279 return null; 280 } 281 $read = [$this->stream]; 282 $write = null; 283 $except = null; 284 if (stream_select($read, $write, $except, $timeoutSeconds, $timeoutMicroseconds)) { 285 $line = fgets($this->stream); 286 $this->lines[] = $line; 287 288 return $line; 289 } else { 290 if ($throwOnTimeout) { 291 throw new LogTimoutException('Timout exceeded when reading line'); 292 } 293 294 return null; 295 } 296 } 297 298 /** 299 * Get all stream read lines. 300 * 301 * @return string[] 302 */ 303 public function getAllLines(): array 304 { 305 return $this->lines; 306 } 307} 308 309class LogFileSource extends LogSource 310{ 311 /** 312 * @var string 313 */ 314 private string $filePath; 315 316 /** 317 * @var int 318 */ 319 private int $position; 320 321 /** 322 * @var array 323 */ 324 private array $lines = []; 325 326 public function __construct(string $filePath) 327 { 328 $this->filePath = $filePath; 329 $this->position = 0; 330 } 331 332 /** 333 * Get single line from the file. 334 * 335 * @param int $timeoutSeconds Read timeout in seconds 336 * @param int $timeoutMicroseconds Read timeout in microseconds 337 * @param bool $throwOnTimeout Whether to throw an exception on timeout 338 * 339 * @return string|null 340 * @throws LogTimoutException 341 */ 342 public function getLine( 343 int $timeoutSeconds, 344 int $timeoutMicroseconds, 345 bool $throwOnTimeout = false 346 ): ?string { 347 $endTime = microtime(true) + $timeoutSeconds + ($timeoutMicroseconds / 1_000_000); 348 while ($this->position >= count($this->lines)) { 349 if (is_file($this->filePath)) { 350 $lines = file($this->filePath); 351 if ($lines === false) { 352 return null; 353 } 354 $this->lines = $lines; 355 if ($this->position < count($lines)) { 356 break; 357 } 358 } 359 usleep(50_000); 360 if (microtime(true) > $endTime) { 361 if ($throwOnTimeout) { 362 throw new LogTimoutException('Timout exceeded when reading line'); 363 } 364 365 return null; 366 } 367 } 368 369 return $this->lines[$this->position++]; 370 } 371 372 /** 373 * Get all returned lines from the file. 374 * 375 * @return string[] 376 */ 377 public function getAllLines(): array 378 { 379 return array_slice($this->lines, 0, $this->position); 380 } 381} 382