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 private 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 $timeoutSeconds Timeout in seconds for reading of all messages. 159 * @param int $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 = 3, 169 int $timeoutMicroseconds = 0 170 ): bool { 171 $startTime = microtime(true); 172 $endTime = $startTime + $timeoutSeconds + ($timeoutMicroseconds / 1_000_000); 173 if ($checkAllLogs) { 174 foreach ($this->getSource()->getAllLines() as $line) { 175 if ($matcher($line)) { 176 return true; 177 } 178 } 179 } 180 181 do { 182 if (microtime(true) > $endTime) { 183 return $this->printError($notFoundMessage); 184 } 185 $line = $this->getLine($timeoutSeconds, $timeoutMicroseconds); 186 if ($line === null || microtime(true) > $endTime) { 187 return $this->printError($notFoundMessage); 188 } 189 } while ( ! $matcher($line)); 190 191 return true; 192 } 193 194 /** 195 * Print tracing message - only in debug . 196 * 197 * @param string $msg Message to print. 198 */ 199 private function trace(string $msg): void 200 { 201 if ($this->debug) { 202 print "LogReader - $msg"; 203 } 204 } 205} 206 207class LogTimoutException extends \Exception 208{ 209} 210 211abstract class LogSource 212{ 213 /** 214 * Get single line from the source. 215 * 216 * @param int $timeoutSeconds Read timeout in seconds 217 * @param int $timeoutMicroseconds Read timeout in microseconds 218 * @param bool $throwOnTimeout Whether to throw an exception on timeout 219 * 220 * @return string|null 221 * @throws LogTimoutException 222 */ 223 public abstract function getLine( 224 int $timeoutSeconds, 225 int $timeoutMicroseconds, 226 bool $throwOnTimeout = false 227 ): ?string; 228 229 /** 230 * Get all lines that has been returned by getLine() method. 231 * 232 * @return string[] 233 */ 234 public abstract function getAllLines(): array; 235} 236 237class LogStreamSource extends LogSource 238{ 239 /** 240 * @var resource 241 */ 242 private $stream; 243 244 /** 245 * @var array 246 */ 247 private array $lines = []; 248 249 public function __construct($stream) 250 { 251 $this->stream = $stream; 252 } 253 254 /** 255 * Get single line from the stream. 256 * 257 * @param int $timeoutSeconds Read timeout in seconds 258 * @param int $timeoutMicroseconds Read timeout in microseconds 259 * @param bool $throwOnTimeout Whether to throw an exception on timeout 260 * 261 * @return string|null 262 * @throws LogTimoutException 263 */ 264 public function getLine( 265 int $timeoutSeconds, 266 int $timeoutMicroseconds, 267 bool $throwOnTimeout = false 268 ): ?string { 269 if (feof($this->stream)) { 270 return null; 271 } 272 $read = [$this->stream]; 273 $write = null; 274 $except = null; 275 if (stream_select($read, $write, $except, $timeoutSeconds, $timeoutMicroseconds)) { 276 $line = fgets($this->stream); 277 $this->lines[] = $line; 278 279 return $line; 280 } else { 281 if ($throwOnTimeout) { 282 throw new LogTimoutException('Timout exceeded when reading line'); 283 } 284 285 return null; 286 } 287 } 288 289 /** 290 * Get all stream read lines. 291 * 292 * @return string[] 293 */ 294 public function getAllLines(): array 295 { 296 return $this->lines; 297 } 298} 299 300class LogFileSource extends LogSource 301{ 302 /** 303 * @var string 304 */ 305 private string $filePath; 306 307 /** 308 * @var int 309 */ 310 private int $position; 311 312 /** 313 * @var array 314 */ 315 private array $lines = []; 316 317 public function __construct(string $filePath) 318 { 319 $this->filePath = $filePath; 320 $this->position = 0; 321 } 322 323 /** 324 * Get single line from the file. 325 * 326 * @param int $timeoutSeconds Read timeout in seconds 327 * @param int $timeoutMicroseconds Read timeout in microseconds 328 * @param bool $throwOnTimeout Whether to throw an exception on timeout 329 * 330 * @return string|null 331 * @throws LogTimoutException 332 */ 333 public function getLine( 334 int $timeoutSeconds, 335 int $timeoutMicroseconds, 336 bool $throwOnTimeout = false 337 ): ?string { 338 $endTime = microtime(true) + $timeoutSeconds + ($timeoutMicroseconds / 1_000_000); 339 while ($this->position >= count($this->lines)) { 340 if (is_file($this->filePath)) { 341 $lines = file($this->filePath); 342 if ($lines === false) { 343 return null; 344 } 345 $this->lines = $lines; 346 if ($this->position < count($lines)) { 347 break; 348 } 349 } 350 usleep(50_000); 351 if (microtime(true) > $endTime) { 352 if ($throwOnTimeout) { 353 throw new LogTimoutException('Timout exceeded when reading line'); 354 } 355 356 return null; 357 } 358 } 359 360 return $this->lines[$this->position++]; 361 } 362 363 /** 364 * Get all returned lines from the file. 365 * 366 * @return string[] 367 */ 368 public function getAllLines(): array 369 { 370 return array_slice($this->lines, 0, $this->position); 371 } 372} 373