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 = null; 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 if (empty($this->sources)) { 120 return; 121 } 122 $hasMultipleDescriptors = count($this->sources) > 1; 123 echo "\nLOGS:\n"; 124 foreach ($this->sources as $name => $source) { 125 if ($hasMultipleDescriptors) { 126 echo ">>> source: $name\n"; 127 } 128 $this->printSeparator(); 129 foreach ($source->getAllLines() as $line) { 130 echo $line; 131 } 132 $this->printSeparator(); 133 } 134 echo "\n"; 135 } 136 137 /** 138 * Print error and logs. 139 * 140 * @param string|null $errorMessage Error message to print before the logs. 141 * 142 * @return false 143 */ 144 public function printError(?string $errorMessage): bool 145 { 146 if (is_null($errorMessage)) { 147 return false; 148 } 149 echo "ERROR: " . $errorMessage . "\n"; 150 $this->printLogs(); 151 152 return false; 153 } 154 155 /** 156 * Read log until matcher matches the log message or there are no more logs. 157 * 158 * @param callable $matcher Callback to identify a match 159 * @param string|null $notFoundMessage Error message if matcher does not succeed. 160 * @param bool $checkAllLogs Whether to also check past logs. 161 * @param int|null $timeoutSeconds Timeout in seconds for reading of all messages. 162 * @param int|null $timeoutMicroseconds Additional timeout in microseconds for reading of all messages. 163 * 164 * @return bool 165 * @throws \Exception 166 */ 167 public function readUntil( 168 callable $matcher, 169 ?string $notFoundMessage = null, 170 bool $checkAllLogs = false, 171 ?int $timeoutSeconds = null, 172 ?int $timeoutMicroseconds = null 173 ): bool { 174 if (is_null($timeoutSeconds) && is_null($timeoutMicroseconds)) { 175 $timeoutSeconds = 3; 176 $timeoutMicroseconds = 0; 177 } elseif (is_null($timeoutSeconds)) { 178 $timeoutSeconds = 0; 179 } elseif (is_null($timeoutMicroseconds)) { 180 $timeoutMicroseconds = 0; 181 } 182 183 $startTime = microtime(true); 184 $endTime = $startTime + $timeoutSeconds + ($timeoutMicroseconds / 1_000_000); 185 if ($checkAllLogs) { 186 foreach ($this->getSource()->getAllLines() as $line) { 187 if ($matcher($line)) { 188 return true; 189 } 190 } 191 } 192 193 do { 194 if (microtime(true) > $endTime) { 195 return $this->printError($notFoundMessage); 196 } 197 $line = $this->getLine($timeoutSeconds, $timeoutMicroseconds); 198 if ($line === null || microtime(true) > $endTime) { 199 return $this->printError($notFoundMessage); 200 } 201 } while ( ! $matcher($line)); 202 203 return true; 204 } 205 206 /** 207 * Print tracing message - only in debug . 208 * 209 * @param string $msg Message to print. 210 */ 211 private function trace(string $msg): void 212 { 213 if ($this->debug) { 214 print "LogReader - $msg"; 215 } 216 } 217} 218 219class LogTimoutException extends \Exception 220{ 221} 222 223abstract class LogSource 224{ 225 /** 226 * Get single line from the source. 227 * 228 * @param int $timeoutSeconds Read timeout in seconds 229 * @param int $timeoutMicroseconds Read timeout in microseconds 230 * @param bool $throwOnTimeout Whether to throw an exception on timeout 231 * 232 * @return string|null 233 * @throws LogTimoutException 234 */ 235 public abstract function getLine( 236 int $timeoutSeconds, 237 int $timeoutMicroseconds, 238 bool $throwOnTimeout = false 239 ): ?string; 240 241 /** 242 * Get all lines that has been returned by getLine() method. 243 * 244 * @return string[] 245 */ 246 public abstract function getAllLines(): array; 247} 248 249class LogStreamSource extends LogSource 250{ 251 /** 252 * @var resource 253 */ 254 private $stream; 255 256 /** 257 * @var array 258 */ 259 private array $lines = []; 260 261 public function __construct($stream) 262 { 263 $this->stream = $stream; 264 } 265 266 /** 267 * Get single line from the stream. 268 * 269 * @param int $timeoutSeconds Read timeout in seconds 270 * @param int $timeoutMicroseconds Read timeout in microseconds 271 * @param bool $throwOnTimeout Whether to throw an exception on timeout 272 * 273 * @return string|null 274 * @throws LogTimoutException 275 */ 276 public function getLine( 277 int $timeoutSeconds, 278 int $timeoutMicroseconds, 279 bool $throwOnTimeout = false 280 ): ?string { 281 if (feof($this->stream)) { 282 return null; 283 } 284 $read = [$this->stream]; 285 $write = null; 286 $except = null; 287 if (stream_select($read, $write, $except, $timeoutSeconds, $timeoutMicroseconds)) { 288 $line = fgets($this->stream); 289 $this->lines[] = $line; 290 291 return $line; 292 } else { 293 if ($throwOnTimeout) { 294 throw new LogTimoutException('Timout exceeded when reading line'); 295 } 296 297 return null; 298 } 299 } 300 301 /** 302 * Get all stream read lines. 303 * 304 * @return string[] 305 */ 306 public function getAllLines(): array 307 { 308 return $this->lines; 309 } 310} 311 312class LogFileSource extends LogSource 313{ 314 /** 315 * @var string 316 */ 317 private string $filePath; 318 319 /** 320 * @var int 321 */ 322 private int $position; 323 324 /** 325 * @var array 326 */ 327 private array $lines = []; 328 329 public function __construct(string $filePath) 330 { 331 $this->filePath = $filePath; 332 $this->position = 0; 333 } 334 335 /** 336 * Get single line from the file. 337 * 338 * @param int $timeoutSeconds Read timeout in seconds 339 * @param int $timeoutMicroseconds Read timeout in microseconds 340 * @param bool $throwOnTimeout Whether to throw an exception on timeout 341 * 342 * @return string|null 343 * @throws LogTimoutException 344 */ 345 public function getLine( 346 int $timeoutSeconds, 347 int $timeoutMicroseconds, 348 bool $throwOnTimeout = false 349 ): ?string { 350 $endTime = microtime(true) + $timeoutSeconds + ($timeoutMicroseconds / 1_000_000); 351 while ($this->position >= count($this->lines)) { 352 if (is_file($this->filePath)) { 353 $lines = file($this->filePath); 354 if ($lines === false) { 355 return null; 356 } 357 $this->lines = $lines; 358 if ($this->position < count($lines)) { 359 break; 360 } 361 } 362 usleep(50_000); 363 if (microtime(true) > $endTime) { 364 if ($throwOnTimeout) { 365 throw new LogTimoutException('Timout exceeded when reading line'); 366 } 367 368 return null; 369 } 370 } 371 372 return $this->lines[$this->position++]; 373 } 374 375 /** 376 * Get all returned lines from the file. 377 * 378 * @return string[] 379 */ 380 public function getAllLines(): array 381 { 382 return array_slice($this->lines, 0, $this->position); 383 } 384} 385