1 <?php declare(strict_types=1);
4 * This file is part of the Monolog package.
6 * (c) Jordi Boggiano <j.boggiano@seld.be>
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
12 namespace Monolog\Handler;
18 * Stores to any stream resource
20 * Can be used to store into php://stderr, remote and local files, etc.
22 * @author Jordi Boggiano <j.boggiano@seld.be>
24 * @phpstan-import-type FormattedRecord from AbstractProcessingHandler
26 class StreamHandler extends AbstractProcessingHandler
29 protected const MAX_CHUNK_SIZE = 2147483647;
30 /** @const int 10MB */
31 protected const DEFAULT_CHUNK_SIZE = 10 * 1024 * 1024;
33 protected $streamChunkSize;
34 /** @var resource|null */
37 protected $url = null;
39 private $errorMessage = null;
41 protected $filePermission;
43 protected $useLocking;
45 private $dirCreated = null;
48 * @param resource|string $stream If a missing path can't be created, an UnexpectedValueException will be thrown on first write
49 * @param int|null $filePermission Optional file permissions (default (0644) are only for owner read/write)
50 * @param bool $useLocking Try to lock log file before doing any writes
52 * @throws \InvalidArgumentException If stream is not a resource or string
54 public function __construct($stream, $level = Logger::DEBUG, bool $bubble = true, ?int $filePermission = null, bool $useLocking = false)
56 parent::__construct($level, $bubble);
58 if (($phpMemoryLimit = Utils::expandIniShorthandBytes(ini_get('memory_limit'))) !== false) {
59 if ($phpMemoryLimit > 0) {
60 // use max 10% of allowed memory for the chunk size, and at least 100KB
61 $this->streamChunkSize = min(static::MAX_CHUNK_SIZE, max((int) ($phpMemoryLimit / 10), 100 * 1024));
63 // memory is unlimited, set to the default 10MB
64 $this->streamChunkSize = static::DEFAULT_CHUNK_SIZE;
67 // no memory limit information, set to the default 10MB
68 $this->streamChunkSize = static::DEFAULT_CHUNK_SIZE;
71 if (is_resource($stream)) {
72 $this->stream = $stream;
74 stream_set_chunk_size($this->stream, $this->streamChunkSize);
75 } elseif (is_string($stream)) {
76 $this->url = Utils::canonicalizePath($stream);
78 throw new \InvalidArgumentException('A stream must either be a resource or a string.');
81 $this->filePermission = $filePermission;
82 $this->useLocking = $useLocking;
88 public function close(): void
90 if ($this->url && is_resource($this->stream)) {
91 fclose($this->stream);
94 $this->dirCreated = null;
98 * Return the currently active stream if it is open
100 * @return resource|null
102 public function getStream()
104 return $this->stream;
108 * Return the stream URL if it was configured with a URL and not an active resource
110 * @return string|null
112 public function getUrl(): ?string
120 public function getStreamChunkSize(): int
122 return $this->streamChunkSize;
128 protected function write(array $record): void
130 if (!is_resource($this->stream)) {
132 if (null === $url || '' === $url) {
133 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));
135 $this->createDir($url);
136 $this->errorMessage = null;
137 set_error_handler([$this, 'customErrorHandler']);
139 $stream = fopen($url, 'a');
140 if ($this->filePermission !== null) {
141 @chmod($url, $this->filePermission);
144 restore_error_handler();
146 if (!is_resource($stream)) {
147 $this->stream = null;
149 throw new \UnexpectedValueException(sprintf('The stream or file "%s" could not be opened in append mode: '.$this->errorMessage, $url) . Utils::getRecordMessageForException($record));
151 stream_set_chunk_size($stream, $this->streamChunkSize);
152 $this->stream = $stream;
155 $stream = $this->stream;
156 if (!is_resource($stream)) {
157 throw new \LogicException('No stream was opened yet' . Utils::getRecordMessageForException($record));
160 if ($this->useLocking) {
161 // ignoring errors here, there's not much we can do about them
162 flock($stream, LOCK_EX);
165 $this->streamWrite($stream, $record);
167 if ($this->useLocking) {
168 flock($stream, LOCK_UN);
174 * @param resource $stream
175 * @param array $record
177 * @phpstan-param FormattedRecord $record
179 protected function streamWrite($stream, array $record): void
181 fwrite($stream, (string) $record['formatted']);
184 private function customErrorHandler(int $code, string $msg): bool
186 $this->errorMessage = preg_replace('{^(fopen|mkdir)\(.*?\): }', '', $msg);
191 private function getDirFromStream(string $stream): ?string
193 $pos = strpos($stream, '://');
194 if ($pos === false) {
195 return dirname($stream);
198 if ('file://' === substr($stream, 0, 7)) {
199 return dirname(substr($stream, 7));
205 private function createDir(string $url): void
207 // Do not try to create dir if it has already been tried.
208 if ($this->dirCreated) {
212 $dir = $this->getDirFromStream($url);
213 if (null !== $dir && !is_dir($dir)) {
214 $this->errorMessage = null;
215 set_error_handler([$this, 'customErrorHandler']);
216 $status = mkdir($dir, 0777, true);
217 restore_error_handler();
218 if (false === $status && !is_dir($dir) && strpos((string) $this->errorMessage, 'File exists') === false) {
219 throw new \UnexpectedValueException(sprintf('There is no existing directory at "%s" and it could not be created: '.$this->errorMessage, $dir));
222 $this->dirCreated = true;