diff --git a/src/Monolog/Handler/SyslogUdp/UdpSocket.php b/src/Monolog/Handler/SyslogUdp/UdpSocket.php new file mode 100644 index 00000000..bf4e1cf3 --- /dev/null +++ b/src/Monolog/Handler/SyslogUdp/UdpSocket.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler\SyslogUdp; + +class UdpSocket +{ + const DATAGRAM_MAX_LENGTH = 2048; + + public function __construct($ip, $port) + { + $this->ip = $ip; + $this->port = $port; + $this->socket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP); + } + + public function write($line, $header = "") + { + $remaining = $line; + while (!is_null($remaining)) { + list($chunk, $remaining) = $this->splitLineIfNessesary($remaining, $header); + $this->send($chunk); + } + } + + public function close() + { + socket_close($this->socket); + } + + protected function send($chunk) + { + socket_sendto($this->socket, $chunk, strlen($chunk), $flags = 0, $this->ip, $this->port); + } + + protected function splitLineIfNessesary($line, $header) + { + if ($this->shouldSplitLine($line, $header)) { + $chunkSize = self::DATAGRAM_MAX_LENGTH - strlen($header); + $chunk = $header . substr($line, 0, $chunkSize); + $remaining = substr($line, $chunkSize); + } else { + $chunk = $header . $line; + $remaining = null; + } + + return [$chunk, $remaining]; + } + + protected function shouldSplitLine($remaining, $header) + { + return strlen($header.$remaining) > self::DATAGRAM_MAX_LENGTH; + } +} diff --git a/src/Monolog/Handler/SyslogUdpHandler.php b/src/Monolog/Handler/SyslogUdpHandler.php new file mode 100644 index 00000000..8cb6e6c3 --- /dev/null +++ b/src/Monolog/Handler/SyslogUdpHandler.php @@ -0,0 +1,117 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Logger; +use Monolog\Handler\SyslogUdp\UdpSocket; + +class SyslogUdpHandler extends AbstractProcessingHandler +{ + protected $facility; + + protected $facilities = array( + "local0" => 16, + "local1" => 17, + "local2" => 18, + "local3" => 19, + "local4" => 20, + "local5" => 21, + "local6" => 22, + "local7" => 23 + ); + + protected $severityMap = array( + Logger::EMERGENCY => 0, + Logger::ALERT => 1, + Logger::CRITICAL => 2, + Logger::ERROR => 3, + Logger::WARNING => 4, + Logger::NOTICE => 5, + Logger::INFO => 6, + Logger::DEBUG => 7 + ); + + public function __construct($facility, $syslogdIp, $port = null) + { + $port = is_null($port) ? 514 : $port; + $this->socket = new UdpSocket($syslogdIp, $port); + + $this->validateFacility($facility); + $this->facility = $this->facilities[$facility]; + + } + + protected function validateFacility($facility) + { + if (!is_string($facility) || !array_key_exists($facility, $this->facilities)) { + throw new \InvalidArgumentException("Invalid syslog facility: $facility"); + } + } + + protected function write(array $record) + { + $this->_write(['message' => $record['formatted'], 'priority' => $record['level']]); + } + + protected function _write($event) + { + $lines = $this->splitMessageIntoLines($event['message']); + + $header = $this->makeCommonSyslogHeader($this->getSeverity($event['priority'])); + + foreach ($lines as $line) { + $this->socket->write($line, $header); + } + } + + public function close() + { + $this->socket->close(); + } + + protected function splitMessageIntoLines($message) + { + if (is_array($message)) { + $message = implode("\n", $message); + } + return preg_split('/$\R?^/m', $message); + } + + /** + * Make common syslog header (see rfc5424) + */ + protected function makeCommonSyslogHeader($severity) + { + $priority = $severity + ($this->facility << 3); + return "<$priority>: "; + } + + /** + * Map the Monolog severity levels to syslogd. + */ + protected function getSeverity($priority) + { + if (array_key_exists($priority, $this->severityMap)) { + return $this->severityMap[$priority]; + } else { + return Logger::INFO; + } + } + + /** + * Inject your own socket, mainly used for testing + */ + public function setSocket($socket) + { + $this->socket = $socket; + } +} diff --git a/tests/Monolog/Handler/SyslogUdpHandlerTest.php b/tests/Monolog/Handler/SyslogUdpHandlerTest.php new file mode 100644 index 00000000..8f89fb83 --- /dev/null +++ b/tests/Monolog/Handler/SyslogUdpHandlerTest.php @@ -0,0 +1,39 @@ +setFormatter(new \Monolog\Formatter\ChromePHPFormatter()); + + $socket = $this->getMock('\Monolog\Handler\SyslogUdp\UdpSocket', ['write'], ['lol', 'lol']); + $socket->expects($this->at(0)) + ->method('write') + ->with("lol", "<172>: "); + $socket->expects($this->at(1)) + ->method('write') + ->with("hej", "<172>: "); + + $handler->setSocket($socket); + + $handler->handle($this->getRecordWithMessage("hej\nlol")); + } + + protected function getRecordWithMessage($msg) + { + return ['message' => $msg, 'level' => \Monolog\Logger::WARNING, 'context' => null, 'extra' => [], 'channel' => 'lol']; + } +} diff --git a/tests/Monolog/Handler/UdpSocketTest.php b/tests/Monolog/Handler/UdpSocketTest.php new file mode 100644 index 00000000..4e1ac4e2 --- /dev/null +++ b/tests/Monolog/Handler/UdpSocketTest.php @@ -0,0 +1,45 @@ +getMock('\Monolog\Handler\SyslogUdp\UdpSocket', ['send'], ['lol', 'lol']); + + $socket->expects($this->at(0)) + ->method('send') + ->with("HEADER: The quick brown fox jumps over the lazy dog"); + + $socket->write("The quick brown fox jumps over the lazy dog", "HEADER: "); + } + + public function testWeSplitLongMessages() + { + $socket = $this->getMock('\Monolog\Handler\SyslogUdp\UdpSocket', ['send'], ['lol', 'lol']); + + $socket->expects($this->at(1)) + ->method('send') + ->with("The quick brown fox jumps over the lazy dog"); + + $aStringOfLength2048 = str_repeat("derp", 2048/4); + + $socket->write($aStringOfLength2048."The quick brown fox jumps over the lazy dog"); + } + + public function testAllSplitMessagesHasAHeader() + { + $socket = $this->getMock('\Monolog\Handler\SyslogUdp\UdpSocket', ['send'], ['lol', 'lol']); + + $socket->expects($this->exactly(5)) + ->method('send') + ->with($this->stringStartsWith("HEADER")); + + $aStringOfLength8192 = str_repeat("derp", 2048); + + $socket->write($aStringOfLength8192, "HEADER"); + } +}