Browse Source

Initial version

Pablo Belloc 14 years ago
parent
commit
486cbb78ab

+ 65 - 0
src/Monolog/Handler/SocketHandler.php

@@ -0,0 +1,65 @@
+<?php
+/**
+ * @author Pablo de Leon Belloc <pablolb@gmail.com>
+ */
+
+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']);
+    }
+}

+ 10 - 0
src/Monolog/Handler/SocketHandler/Exception/ConnectionException.php

@@ -0,0 +1,10 @@
+<?php
+/**
+ * @author Pablo de Leon Belloc <pablolb@gmail.com>
+ */
+
+namespace Monolog\Handler\SocketHandler\Exception;
+
+class ConnectionException extends \RuntimeException
+{
+}

+ 11 - 0
src/Monolog/Handler/SocketHandler/Exception/WriteToSocketException.php

@@ -0,0 +1,11 @@
+<?php
+/**
+ * @author Pablo de Leon Belloc <pablolb@gmail.com>
+ */
+
+
+namespace Monolog\Handler\SocketHandler\Exception;
+
+class WriteToSocketException extends \RuntimeException
+{
+}

+ 60 - 0
src/Monolog/Handler/SocketHandler/MockSocket.php

@@ -0,0 +1,60 @@
+<?php
+/**
+ * @author Pablo de Leon Belloc <pablolb@gmail.com>
+ */
+
+
+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;
+    }
+}

+ 24 - 0
src/Monolog/Handler/SocketHandler/PersistentSocket.php

@@ -0,0 +1,24 @@
+<?php
+/**
+ * @author Pablo de Leon Belloc <pablolb@gmail.com>
+ */
+
+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;
+    }
+}

+ 178 - 0
src/Monolog/Handler/SocketHandler/Socket.php

@@ -0,0 +1,178 @@
+<?php
+/**
+ * @author Pablo de Leon Belloc <pablolb@gmail.com>
+ */
+
+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)");
+        }
+    }
+}

+ 114 - 0
tests/Monolog/Handler/SocketHandler/SocketTest.php

@@ -0,0 +1,114 @@
+<?php
+
+namespace Monolog\Handler\SocketHandler;
+
+class SocketTest extends \PHPUnit_Framework_TestCase
+{
+    
+    /**
+     * @expectedException Monolog\Handler\SocketHandler\Exception\ConnectionException
+     */
+    public function testInvalidHostname() {
+        $socket = new Socket('garbage://here');
+        $socket->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('');
+    }
+}

+ 54 - 0
tests/Monolog/Handler/SocketHandlerTest.php

@@ -0,0 +1,54 @@
+<?php
+/**
+ * @author Pablo de Leon Belloc <pablolb@gmail.com>
+ */
+
+
+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());
+    }
+    
+}