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;
17 * Stores to any socket - uses fsockopen() or pfsockopen().
19 * @author Pablo de Leon Belloc <pablolb@gmail.com>
20 * @see http://php.net/manual/en/function.fsockopen.php
22 class SocketHandler extends AbstractProcessingHandler
24 private $connectionString;
25 private $connectionTimeout;
28 private $writingTimeout = 10;
29 private $lastSentBytes = null;
30 private $persistent = false;
33 private $lastWritingAt;
36 * @param string $connectionString Socket connection string
37 * @param int $level The minimum logging level at which this handler will be triggered
38 * @param Boolean $bubble Whether the messages that are handled can bubble up the stack or not
40 public function __construct($connectionString, $level = Logger::DEBUG, $bubble = true)
42 parent::__construct($level, $bubble);
43 $this->connectionString = $connectionString;
44 $this->connectionTimeout = (float) ini_get('default_socket_timeout');
48 * Connect (if necessary) and write to the socket
50 * @param array $record
52 * @throws \UnexpectedValueException
53 * @throws \RuntimeException
55 protected function write(array $record)
57 $this->connectIfNotConnected();
58 $data = $this->generateDataStream($record);
59 $this->writeToSocket($data);
63 * We will not close a PersistentSocket instance so it can be reused in other requests.
65 public function close()
67 if (!$this->isPersistent()) {
73 * Close socket, if open
75 public function closeSocket()
77 if (is_resource($this->resource)) {
78 fclose($this->resource);
79 $this->resource = null;
84 * Set socket connection to nbe persistent. It only has effect before the connection is initiated.
86 * @param bool $persistent
88 public function setPersistent($persistent)
90 $this->persistent = (boolean) $persistent;
94 * Set connection timeout. Only has effect before we connect.
96 * @param float $seconds
98 * @see http://php.net/manual/en/function.fsockopen.php
100 public function setConnectionTimeout($seconds)
102 $this->validateTimeout($seconds);
103 $this->connectionTimeout = (float) $seconds;
107 * Set write timeout. Only has effect before we connect.
109 * @param float $seconds
111 * @see http://php.net/manual/en/function.stream-set-timeout.php
113 public function setTimeout($seconds)
115 $this->validateTimeout($seconds);
116 $this->timeout = (float) $seconds;
120 * Set writing timeout. Only has effect during connection in the writing cycle.
122 * @param float $seconds 0 for no timeout
124 public function setWritingTimeout($seconds)
126 $this->validateTimeout($seconds);
127 $this->writingTimeout = (float) $seconds;
131 * Get current connection string
135 public function getConnectionString()
137 return $this->connectionString;
141 * Get persistent setting
145 public function isPersistent()
147 return $this->persistent;
151 * Get current connection timeout setting
155 public function getConnectionTimeout()
157 return $this->connectionTimeout;
161 * Get current in-transfer timeout
165 public function getTimeout()
167 return $this->timeout;
171 * Get current local writing timeout
175 public function getWritingTimeout()
177 return $this->writingTimeout;
181 * Check to see if the socket is currently available.
183 * UDP might appear to be connected but might fail when writing. See http://php.net/fsockopen for details.
187 public function isConnected()
189 return is_resource($this->resource)
190 && !feof($this->resource); // on TCP - other party can close connection.
194 * Wrapper to allow mocking
196 protected function pfsockopen()
198 return @pfsockopen($this->connectionString, -1, $this->errno, $this->errstr, $this->connectionTimeout);
202 * Wrapper to allow mocking
204 protected function fsockopen()
206 return @fsockopen($this->connectionString, -1, $this->errno, $this->errstr, $this->connectionTimeout);
210 * Wrapper to allow mocking
212 * @see http://php.net/manual/en/function.stream-set-timeout.php
214 protected function streamSetTimeout()
216 $seconds = floor($this->timeout);
217 $microseconds = round(($this->timeout - $seconds) * 1e6);
219 return stream_set_timeout($this->resource, $seconds, $microseconds);
223 * Wrapper to allow mocking
225 protected function fwrite($data)
227 return @fwrite($this->resource, $data);
231 * Wrapper to allow mocking
233 protected function streamGetMetadata()
235 return stream_get_meta_data($this->resource);
238 private function validateTimeout($value)
240 $ok = filter_var($value, FILTER_VALIDATE_FLOAT);
241 if ($ok === false || $value < 0) {
242 throw new \InvalidArgumentException("Timeout must be 0 or a positive float (got $value)");
246 private function connectIfNotConnected()
248 if ($this->isConnected()) {
254 protected function generateDataStream($record)
256 return (string) $record['formatted'];
260 * @return resource|null
262 protected function getResource()
264 return $this->resource;
267 private function connect()
269 $this->createSocketResource();
270 $this->setSocketTimeout();
273 private function createSocketResource()
275 if ($this->isPersistent()) {
276 $resource = $this->pfsockopen();
278 $resource = $this->fsockopen();
281 throw new \UnexpectedValueException("Failed connecting to $this->connectionString ($this->errno: $this->errstr)");
283 $this->resource = $resource;
286 private function setSocketTimeout()
288 if (!$this->streamSetTimeout()) {
289 throw new \UnexpectedValueException("Failed setting timeout with stream_set_timeout()");
293 private function writeToSocket($data)
295 $length = strlen($data);
297 $this->lastSentBytes = $sent;
298 while ($this->isConnected() && $sent < $length) {
300 $chunk = $this->fwrite($data);
302 $chunk = $this->fwrite(substr($data, $sent));
304 if ($chunk === false) {
305 throw new \RuntimeException("Could not write to socket");
308 $socketInfo = $this->streamGetMetadata();
309 if ($socketInfo['timed_out']) {
310 throw new \RuntimeException("Write timed-out");
313 if ($this->writingIsTimedOut($sent)) {
314 throw new \RuntimeException("Write timed-out, no data sent for `{$this->writingTimeout}` seconds, probably we got disconnected (sent $sent of $length)");
317 if (!$this->isConnected() && $sent < $length) {
318 throw new \RuntimeException("End-of-file reached, probably we got disconnected (sent $sent of $length)");
322 private function writingIsTimedOut($sent)
324 $writingTimeout = (int) floor($this->writingTimeout);
325 if (0 === $writingTimeout) {
329 if ($sent !== $this->lastSentBytes) {
330 $this->lastWritingAt = time();
331 $this->lastSentBytes = $sent;
338 if ((time() - $this->lastWritingAt) >= $writingTimeout) {
339 $this->closeSocket();