vendor/monolog/monolog/src/Monolog/Handler/StreamHandler.php line 25

Open in your IDE?
  1. <?php declare(strict_types=1);
  2. /*
  3.  * This file is part of the Monolog package.
  4.  *
  5.  * (c) Jordi Boggiano <j.boggiano@seld.be>
  6.  *
  7.  * For the full copyright and license information, please view the LICENSE
  8.  * file that was distributed with this source code.
  9.  */
  10. namespace Monolog\Handler;
  11. use Monolog\Level;
  12. use Monolog\Utils;
  13. use Monolog\LogRecord;
  14. /**
  15.  * Stores to any stream resource
  16.  *
  17.  * Can be used to store into php://stderr, remote and local files, etc.
  18.  *
  19.  * @author Jordi Boggiano <j.boggiano@seld.be>
  20.  */
  21. class StreamHandler extends AbstractProcessingHandler
  22. {
  23.     protected const MAX_CHUNK_SIZE 2147483647;
  24.     /** 10MB */
  25.     protected const DEFAULT_CHUNK_SIZE 10 1024 1024;
  26.     protected int $streamChunkSize;
  27.     /** @var resource|null */
  28.     protected $stream;
  29.     protected string|null $url null;
  30.     private string|null $errorMessage null;
  31.     protected int|null $filePermission;
  32.     protected bool $useLocking;
  33.     protected string $fileOpenMode;
  34.     /** @var true|null */
  35.     private bool|null $dirCreated null;
  36.     private bool $retrying false;
  37.     private int|null $inodeUrl null;
  38.     /**
  39.      * @param resource|string $stream         If a missing path can't be created, an UnexpectedValueException will be thrown on first write
  40.      * @param int|null        $filePermission Optional file permissions (default (0644) are only for owner read/write)
  41.      * @param bool            $useLocking     Try to lock log file before doing any writes
  42.      * @param string          $fileOpenMode   The fopen() mode used when opening a file, if $stream is a file path
  43.      *
  44.      * @throws \InvalidArgumentException If stream is not a resource or string
  45.      */
  46.     public function __construct($streamint|string|Level $level Level::Debugbool $bubble true, ?int $filePermission nullbool $useLocking falsestring $fileOpenMode 'a')
  47.     {
  48.         parent::__construct($level$bubble);
  49.         if (($phpMemoryLimit Utils::expandIniShorthandBytes(\ini_get('memory_limit'))) !== false) {
  50.             if ($phpMemoryLimit 0) {
  51.                 // use max 10% of allowed memory for the chunk size, and at least 100KB
  52.                 $this->streamChunkSize min(static::MAX_CHUNK_SIZEmax((int) ($phpMemoryLimit 10), 100 1024));
  53.             } else {
  54.                 // memory is unlimited, set to the default 10MB
  55.                 $this->streamChunkSize = static::DEFAULT_CHUNK_SIZE;
  56.             }
  57.         } else {
  58.             // no memory limit information, set to the default 10MB
  59.             $this->streamChunkSize = static::DEFAULT_CHUNK_SIZE;
  60.         }
  61.         if (\is_resource($stream)) {
  62.             $this->stream $stream;
  63.             stream_set_chunk_size($this->stream$this->streamChunkSize);
  64.         } elseif (\is_string($stream)) {
  65.             $this->url Utils::canonicalizePath($stream);
  66.         } else {
  67.             throw new \InvalidArgumentException('A stream must either be a resource or a string.');
  68.         }
  69.         $this->fileOpenMode $fileOpenMode;
  70.         $this->filePermission $filePermission;
  71.         $this->useLocking $useLocking;
  72.     }
  73.     /**
  74.      * @inheritDoc
  75.      */
  76.     public function reset(): void
  77.     {
  78.         parent::reset();
  79.         // auto-close on reset to make sure we periodically close the file in long running processes
  80.         // as long as they correctly call reset() between jobs
  81.         if ($this->url !== null && $this->url !== 'php://memory') {
  82.             $this->close();
  83.         }
  84.     }
  85.     /**
  86.      * @inheritDoc
  87.      */
  88.     public function close(): void
  89.     {
  90.         if (null !== $this->url && \is_resource($this->stream)) {
  91.             fclose($this->stream);
  92.         }
  93.         $this->stream null;
  94.         $this->dirCreated null;
  95.     }
  96.     /**
  97.      * Return the currently active stream if it is open
  98.      *
  99.      * @return resource|null
  100.      */
  101.     public function getStream()
  102.     {
  103.         return $this->stream;
  104.     }
  105.     /**
  106.      * Return the stream URL if it was configured with a URL and not an active resource
  107.      */
  108.     public function getUrl(): ?string
  109.     {
  110.         return $this->url;
  111.     }
  112.     public function getStreamChunkSize(): int
  113.     {
  114.         return $this->streamChunkSize;
  115.     }
  116.     /**
  117.      * @inheritDoc
  118.      */
  119.     protected function write(LogRecord $record): void
  120.     {
  121.         if ($this->hasUrlInodeWasChanged()) {
  122.             $this->close();
  123.             $this->write($record);
  124.             return;
  125.         }
  126.         if (!\is_resource($this->stream)) {
  127.             $url $this->url;
  128.             if (null === $url || '' === $url) {
  129.                 throw new \LogicException('Missing stream url, the stream can not be opened. This may be caused by a premature call to close().' Utils::getRecordMessageForException($record));
  130.             }
  131.             $this->createDir($url);
  132.             $this->errorMessage null;
  133.             set_error_handler($this->customErrorHandler(...));
  134.             try {
  135.                 $stream fopen($url$this->fileOpenMode);
  136.                 if ($this->filePermission !== null) {
  137.                     @chmod($url$this->filePermission);
  138.                 }
  139.             } finally {
  140.                 restore_error_handler();
  141.             }
  142.             if (!\is_resource($stream)) {
  143.                 $this->stream null;
  144.                 throw new \UnexpectedValueException(sprintf('The stream or file "%s" could not be opened in append mode: '.$this->errorMessage$url) . Utils::getRecordMessageForException($record));
  145.             }
  146.             stream_set_chunk_size($stream$this->streamChunkSize);
  147.             $this->stream $stream;
  148.             $this->inodeUrl $this->getInodeFromUrl();
  149.         }
  150.         $stream $this->stream;
  151.         if ($this->useLocking) {
  152.             // ignoring errors here, there's not much we can do about them
  153.             flock($streamLOCK_EX);
  154.         }
  155.         $this->errorMessage null;
  156.         set_error_handler($this->customErrorHandler(...));
  157.         try {
  158.             $this->streamWrite($stream$record);
  159.         } finally {
  160.             restore_error_handler();
  161.         }
  162.         if ($this->errorMessage !== null) {
  163.             $error $this->errorMessage;
  164.             // close the resource if possible to reopen it, and retry the failed write
  165.             if (!$this->retrying && $this->url !== null && $this->url !== 'php://memory') {
  166.                 $this->retrying true;
  167.                 $this->close();
  168.                 $this->write($record);
  169.                 return;
  170.             }
  171.             throw new \UnexpectedValueException('Writing to the log file failed: '.$error Utils::getRecordMessageForException($record));
  172.         }
  173.         $this->retrying false;
  174.         if ($this->useLocking) {
  175.             flock($streamLOCK_UN);
  176.         }
  177.     }
  178.     /**
  179.      * Write to stream
  180.      * @param resource $stream
  181.      */
  182.     protected function streamWrite($streamLogRecord $record): void
  183.     {
  184.         fwrite($stream, (string) $record->formatted);
  185.     }
  186.     /**
  187.      * @return true
  188.      */
  189.     private function customErrorHandler(int $codestring $msg): bool
  190.     {
  191.         $this->errorMessage preg_replace('{^(fopen|mkdir|fwrite)\(.*?\): }'''$msg);
  192.         return true;
  193.     }
  194.     private function getDirFromStream(string $stream): ?string
  195.     {
  196.         $pos strpos($stream'://');
  197.         if ($pos === false) {
  198.             return \dirname($stream);
  199.         }
  200.         if ('file://' === substr($stream07)) {
  201.             return \dirname(substr($stream7));
  202.         }
  203.         return null;
  204.     }
  205.     private function createDir(string $url): void
  206.     {
  207.         // Do not try to create dir if it has already been tried.
  208.         if (true === $this->dirCreated) {
  209.             return;
  210.         }
  211.         $dir $this->getDirFromStream($url);
  212.         if (null !== $dir && !is_dir($dir)) {
  213.             $this->errorMessage null;
  214.             set_error_handler(function (...$args) {
  215.                 return $this->customErrorHandler(...$args);
  216.             });
  217.             $status mkdir($dir0777true);
  218.             restore_error_handler();
  219.             if (false === $status && !is_dir($dir) && strpos((string) $this->errorMessage'File exists') === false) {
  220.                 throw new \UnexpectedValueException(sprintf('There is no existing directory at "%s" and it could not be created: '.$this->errorMessage$dir));
  221.             }
  222.         }
  223.         $this->dirCreated true;
  224.     }
  225.     private function getInodeFromUrl(): ?int
  226.     {
  227.         if ($this->url === null || str_starts_with($this->url'php://')) {
  228.             return null;
  229.         }
  230.         $inode = @fileinode($this->url);
  231.         return $inode === false null $inode;
  232.     }
  233.     private function hasUrlInodeWasChanged(): bool
  234.     {
  235.         if ($this->inodeUrl === null || $this->retrying || $this->inodeUrl === $this->getInodeFromUrl()) {
  236.             return false;
  237.         }
  238.         $this->retrying true;
  239.         return true;
  240.     }
  241. }