From 486cbb78abcd5e7ebed799b98725826df036023b Mon Sep 17 00:00:00 2001 From: Pablo Belloc Date: Sat, 25 Feb 2012 20:37:04 -0300 Subject: [PATCH] Initial version --- src/Monolog/Handler/SocketHandler.php | 65 +++++++ .../Exception/ConnectionException.php | 10 + .../Exception/WriteToSocketException.php | 11 ++ .../Handler/SocketHandler/MockSocket.php | 60 ++++++ .../SocketHandler/PersistentSocket.php | 24 +++ src/Monolog/Handler/SocketHandler/Socket.php | 178 ++++++++++++++++++ .../Handler/SocketHandler/SocketTest.php | 114 +++++++++++ tests/Monolog/Handler/SocketHandlerTest.php | 54 ++++++ 8 files changed, 516 insertions(+) create mode 100644 src/Monolog/Handler/SocketHandler.php create mode 100644 src/Monolog/Handler/SocketHandler/Exception/ConnectionException.php create mode 100644 src/Monolog/Handler/SocketHandler/Exception/WriteToSocketException.php create mode 100644 src/Monolog/Handler/SocketHandler/MockSocket.php create mode 100644 src/Monolog/Handler/SocketHandler/PersistentSocket.php create mode 100644 src/Monolog/Handler/SocketHandler/Socket.php create mode 100644 tests/Monolog/Handler/SocketHandler/SocketTest.php create mode 100644 tests/Monolog/Handler/SocketHandlerTest.php diff --git a/src/Monolog/Handler/SocketHandler.php b/src/Monolog/Handler/SocketHandler.php new file mode 100644 index 00000000..c778da2f --- /dev/null +++ b/src/Monolog/Handler/SocketHandler.php @@ -0,0 +1,65 @@ + + */ + +namespace Monolog\Handler; + +use Monolog\Handler\SocketHandler\Socket; +use Monolog\Handler\SocketHandler\PersistentSocket; +use Monolog\Logger; + +/** + * Stores to any socket - uses fsockopen() or pfsockopen(). + * + * @see Monolog\Handler\SocketHandler\Socket + * @see Monolog\Handler\SocketHandler\PersistentSocket + * @see http://php.net/manual/en/function.fsockopen.php + */ +class SocketHandler extends AbstractProcessingHandler +{ + /** + * @var Socket + */ + private $socket; + + /** + * @param string $connectionString + * @param integer $level The minimum logging level at which this handler will be triggered + * @param Boolean $bubble Whether the messages that are handled can bubble up the stack or not + */ + public function __construct($connectionString, $level = Logger::DEBUG, $bubble = true) + { + parent::__construct($level, $bubble); + $this->socket = new Socket($connectionString); + } + + /** + * Inject socket - allows you to configure timeouts. + * + * @param Socket $socket + */ + public function setSocket(Socket $socket) + { + $this->socket = $socket; + } + + /** + * We will not close a PersistentSocket instance so it can be reused in other requests. + */ + public function close() + { + if ($this->socket instanceof PersistentSocket) { + return; + } + $this->socket->close(); + } + + /** + * {@inheritdoc} + */ + protected function write(array $record) + { + $this->socket->write((string) $record['formatted']); + } +} diff --git a/src/Monolog/Handler/SocketHandler/Exception/ConnectionException.php b/src/Monolog/Handler/SocketHandler/Exception/ConnectionException.php new file mode 100644 index 00000000..c43541c4 --- /dev/null +++ b/src/Monolog/Handler/SocketHandler/Exception/ConnectionException.php @@ -0,0 +1,10 @@ + + */ + +namespace Monolog\Handler\SocketHandler\Exception; + +class ConnectionException extends \RuntimeException +{ +} \ No newline at end of file diff --git a/src/Monolog/Handler/SocketHandler/Exception/WriteToSocketException.php b/src/Monolog/Handler/SocketHandler/Exception/WriteToSocketException.php new file mode 100644 index 00000000..4843f2e6 --- /dev/null +++ b/src/Monolog/Handler/SocketHandler/Exception/WriteToSocketException.php @@ -0,0 +1,11 @@ + + */ + + +namespace Monolog\Handler\SocketHandler\Exception; + +class WriteToSocketException extends \RuntimeException +{ +} \ No newline at end of file diff --git a/src/Monolog/Handler/SocketHandler/MockSocket.php b/src/Monolog/Handler/SocketHandler/MockSocket.php new file mode 100644 index 00000000..885ffabb --- /dev/null +++ b/src/Monolog/Handler/SocketHandler/MockSocket.php @@ -0,0 +1,60 @@ + + */ + + +namespace Monolog\Handler\SocketHandler; + +use Monolog\Handler\SocketHandler\Exception\ConnectionException; +use Monolog\Handler\SocketHandler\Exception\WriteToSocketException; + +class MockSocket extends Socket +{ + private $connectTimeoutMock = 0; + private $timeoutMock = 0; + + + public function __construct($connectionString) + { + if (is_resource($connectionString)) { + $this->resource = $connectionString; + } else { + $this->connectionString = $connectionString; + } + } + + public function setFailConnectionTimeout($seconds) + { + $this->connectTimeoutMock = (int)$seconds; + } + + public function setFailTimeout($seconds) + { + $this->timeoutMock = (int)$seconds; + } + + protected function createSocketResource() + { + if ($this->connectTimeoutMock > 0) { + throw new ConnectionException("Mocked connection timeout"); + } + $this->resource = fopen('php://memory', '+a'); + } + + protected function writeToSocket($data) { + if ($this->timeoutMock > 0) { + throw new WriteToSocketException("Mocked write timeout"); + } + return parent::writeToSocket($data); + } + + protected function setSocketTimeout() + { + // php://memory does not support this + } + + public function getResource() { + return $this->resource; + } +} diff --git a/src/Monolog/Handler/SocketHandler/PersistentSocket.php b/src/Monolog/Handler/SocketHandler/PersistentSocket.php new file mode 100644 index 00000000..9facb40e --- /dev/null +++ b/src/Monolog/Handler/SocketHandler/PersistentSocket.php @@ -0,0 +1,24 @@ + + */ + +namespace Monolog\Handler\SocketHandler; + +use Monolog\Handler\SocketHandler\Exception\ConnectionException; + +/** + * Same as Socket but uses pfsockopen() instead allowing the connection to be reused in other requests. + * + * @see http://php.net/manual/en/function.pfsockopen.php + */ +class PersistentSocket extends Socket +{ + protected function createSocketResource() { + @$resource = pfsockopen($this->connectionString, -1, $errno, $errstr, $this->connectionTimeout); + if (!$resource) { + throw new ConnectionException("Failed connecting to $this->connectionString ($errno: $errstr)"); + } + $this->resource = $resource; + } +} diff --git a/src/Monolog/Handler/SocketHandler/Socket.php b/src/Monolog/Handler/SocketHandler/Socket.php new file mode 100644 index 00000000..ede89f54 --- /dev/null +++ b/src/Monolog/Handler/SocketHandler/Socket.php @@ -0,0 +1,178 @@ + + */ + +namespace Monolog\Handler\SocketHandler; + +use Monolog\Handler\SocketHandler\Exception\ConnectionException; +use Monolog\Handler\SocketHandler\Exception\WriteToSocketException; + +/** + * Small class which writes to a socket. + * Timeout settings must be set before first write to have any effect. + * + * @see http://php.net/manual/en/function.fsockopen.php + */ +class Socket +{ + protected $connectionString; + protected $connectionTimeout; + protected $resource; + private $timeout = 0; + + /** + * @param string $connectionString As interpreted by fsockopen() + */ + public function __construct($connectionString) + { + $this->connectionString = $connectionString; + $this->connectionTimeout = (float)ini_get('default_socket_timeout'); + } + + public function getConnectionString() + { + return $this->connectionString; + } + + /** + * Set connection timeout. Only has effect before we connect. + * + * @see http://php.net/manual/en/function.fsockopen.php + * @param integer $seconds + */ + public function setConnectionTimeout($seconds) + { + $this->validateTimeout($seconds); + $this->connectionTimeout = (float)$seconds; + } + + /** + * Set write timeout. Only has effect before we connect. + * + * @see http://php.net/manual/en/function.stream-set-timeout.php + * @param type $seconds + */ + public function setTimeout($seconds) + { + $this->validateTimeout($seconds); + $this->timeout = (int)$seconds; + } + + private function validateTimeout($value) + { + $ok = filter_var($value, FILTER_VALIDATE_INT, array('options' => array( + 'min_range' => 0, + ))); + if ($ok === false) { + throw new \InvalidArgumentException("Timeout must be 0 or a positive integer (got $value)"); + } + } + + public function getConnectionTimeout() { + return $this->connectionTimeout; + } + + public function getTimeout() { + return $this->timeout; + } + + public function close() + { + if (is_resource($this->resource)) { + fclose($this->resource); + $this->resource = null; + } + } + + /** + * Allow injecting a resource opened somewhere else. Used in tests. + * + * @throws \InvalidArgumentException + * @param resource $resource + */ + public function setResource($resource) + { + if (is_resource($resource)) { + $this->resource = $resource; + } else { + throw new \InvalidArgumentException("Expected a resource"); + } + } + + /** + * Connect (if necessary) and write to the socket + * + * @throws Monolog\Handler\SocketHandler\Exception\ConnectionException + * @throws Monolog\Handler\SocketHandler\Exception\WriteToSocketException + * @param string $string + */ + public function write($string) + { + $this->connectIfNotConnected(); + $this->writeToSocket($string); + } + + protected function connectIfNotConnected() + { + if ($this->isConnected()) { + return; + } + $this->connect(); + } + + /** + * Check to see if the socket is currently available. + * + * UDP might appear to be connected but might fail when writing. See http://php.net/fsockopen for details. + * + * @return boolean + */ + public function isConnected() + { + return is_resource($this->resource) + && !feof($this->resource); // on TCP - other party can close connection. + } + + protected function connect() + { + $this->createSocketResource(); + $this->setSocketTimeout(); + } + + protected function createSocketResource() + { + @$resource = fsockopen($this->connectionString, -1, $errno, $errstr, $this->connectionTimeout); + if (!$resource) { + throw new ConnectionException("Failed connecting to $this->connectionString ($errno: $errstr)"); + } + $this->resource = $resource; + } + + protected function setSocketTimeout() + { + if (!stream_set_timeout($this->resource, $this->timeout)) { + throw new ConnectionException("Failed setting timeout with stream_set_timeout()"); + } + } + + protected function writeToSocket($data) + { + $length = strlen($data); + $sent = 0; + while ($this->isConnected() && $sent < $length) { + @$chunk = fwrite($this->resource, substr($data, $sent)); + if ($chunk === false) { + throw new WriteToSocketException("Could not write to socket"); + } + $sent += $chunk; + $socketInfo = stream_get_meta_data($this->resource); + if ($socketInfo['timed_out']) { + throw new WriteToSocketException("Write timed-out"); + } + } + if (!$this->isConnected() && $sent < $length) { + throw new WriteToSocketException("End-of-file reached, probably we got disconnected (sent $sent of $length)"); + } + } +} diff --git a/tests/Monolog/Handler/SocketHandler/SocketTest.php b/tests/Monolog/Handler/SocketHandler/SocketTest.php new file mode 100644 index 00000000..7e12cbb6 --- /dev/null +++ b/tests/Monolog/Handler/SocketHandler/SocketTest.php @@ -0,0 +1,114 @@ +write('data'); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testBadConnectionTimeout() + { + $socket = new Socket('localhost:1234'); + $socket->setConnectionTimeout(-1); + } + + public function testSetConnectionTimeout() + { + $socket = new Socket('localhost:1234'); + $socket->setConnectionTimeout(10); + $this->assertEquals(10, $socket->getConnectionTimeout()); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testBadTimeout() + { + $socket = new Socket('localhost:1234'); + $socket->setTimeout(-1); + } + + public function testSetTimeout() + { + $socket = new Socket('localhost:1234'); + $socket->setTimeout(10); + $this->assertEquals(10, $socket->getTimeout()); + } + + public function testSetConnectionString() + { + $socket = new Socket('tcp://localhost:9090'); + $this->assertEquals('tcp://localhost:9090', $socket->getConnectionString()); + } + + public function testConnectionRefuesed() + { + try { + $socket = new Socket('127.0.0.1:7894'); + $socket->setTimeout(1); + $string = 'Hello world'; + $socket->write($string); + $this->fail("Shoul not connect - are you running a server on 127.0.0.1:7894 ?"); + } catch (\Monolog\Handler\SocketHandler\Exception\ConnectionException $e) { + } + } + + /** + * @expectedException Monolog\Handler\SocketHandler\Exception\ConnectionException + */ + public function testConnectionTimeoutWithMock() + { + $socket = new MockSocket('localhost:54321'); + $socket->setConnectionTimeout(10); + $socket->setFailConnectionTimeout(10); + $socket->write('Hello world'); + } + + /** + * @expectedException Monolog\Handler\SocketHandler\Exception\WriteToSocketException + */ + public function testWriteTimeoutWithMock() + { + $socket = new MockSocket('localhost:54321'); + $socket->setTimeout(10); + $socket->setFailTimeout(10); + $socket->write('Hello world'); + } + + public function testWriteWithMock() + { + $socket = new MockSocket('localhost:54321'); + $socket->write('Hello world'); + $res = $socket->getResource(); + fseek($res, 0); + $this->assertEquals('Hello world', fread($res, 1024)); + } + + public function testClose() + { + $resource = fopen('php://memory', 'a+'); + $socket = new MockSocket($resource); + $this->assertTrue(is_resource($resource)); + $socket->close(); + $this->assertFalse(is_resource($resource)); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testInjectBadResourceThrowsException() + { + $socket = new Socket(''); + $socket->setResource(''); + } +} \ No newline at end of file diff --git a/tests/Monolog/Handler/SocketHandlerTest.php b/tests/Monolog/Handler/SocketHandlerTest.php new file mode 100644 index 00000000..69eb3d71 --- /dev/null +++ b/tests/Monolog/Handler/SocketHandlerTest.php @@ -0,0 +1,54 @@ + + */ + + +namespace Monolog\Handler; + +use Monolog\Handler\SocketHandler\MockSocket; +use Monolog\Handler\SocketHandler\Socket; +use Monolog\Handler\SocketHandler\PersistentSocket; + +use Monolog\TestCase; +use Monolog\Logger; + +class SocketHandlerTest extends TestCase +{ + public function testWrite() + { + $socket = new MockSocket('localhost'); + $handler = new SocketHandler('localhost'); + $handler->setSocket($socket); + $handler->setFormatter($this->getIdentityFormatter()); + $handler->handle($this->getRecord(Logger::WARNING, 'test')); + $handler->handle($this->getRecord(Logger::WARNING, 'test2')); + $handler->handle($this->getRecord(Logger::WARNING, 'test3')); + $handle = $socket->getResource(); + fseek($handle, 0); + $this->assertEquals('testtest2test3', fread($handle, 100)); + } + + public function testCloseClosesNonPersistentSocket() + { + $socket = new Socket('localhost'); + $res = fopen('php://memory', 'a'); + $socket->setResource($res); + $handler = new SocketHandler('localhost'); + $handler->setSocket($socket); + $handler->close(); + $this->assertFalse($socket->isConnected()); + } + + public function testCloseDoesNotClosePersistentSocket() + { + $socket = new PersistentSocket('localhost'); + $res = fopen('php://memory', 'a'); + $socket->setResource($res); + $handler = new SocketHandler('localhost'); + $handler->setSocket($socket); + $handler->close(); + $this->assertTrue($socket->isConnected()); + } + +}