diff --git a/src/Monolog/Formatter/SyslogFormatter.php b/src/Monolog/Formatter/SyslogFormatter.php new file mode 100644 index 00000000..0d69fabe --- /dev/null +++ b/src/Monolog/Formatter/SyslogFormatter.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Formatter; + +use Monolog\Handler\Syslog\SyslogUtils; +use Monolog\Level; +use Monolog\LogRecord; + +/** + * Serializes a log message according to RFC 5424 + * + * @author Dalibor Karlović + * @author Renat Gabdullin + */ +class SyslogFormatter extends LineFormatter +{ + private const SYSLOG_FACILITY_USER = 1; + private const FORMAT = "<%extra.priority%>1 %datetime% %extra.hostname% %extra.app-name% %extra.procid% %channel% %extra.structured-data% %level_name%: %message% %context% %extra%\n"; + private const NILVALUE = '-'; + + private string $hostname; + private int $procid; + + public function __construct(private string $applicationName = self::NILVALUE) + { + parent::__construct(self::FORMAT, 'Y-m-d\TH:i:s.uP', true, true); + $this->hostname = (string) gethostname(); + $this->procid = (int) getmypid(); + } + + public function format(LogRecord $record): string + { + $record->extra = $this->formatExtra($record); + + return parent::format($record); + } + + /** + * @param LogRecord $record + * @return array + */ + private function formatExtra(LogRecord $record): array + { + $extra = $record->extra; + $extra['app-name'] = $this->applicationName; + $extra['hostname'] = $this->hostname; + $extra['procid'] = $this->procid; + $extra['priority'] = self::calculatePriority($record->level); + $extra['structured-data'] = self::NILVALUE; + + return $extra; + } + + private static function calculatePriority(Level $level): int + { + return (self::SYSLOG_FACILITY_USER * 8) + SyslogUtils::toSyslogPriority($level); + } +} diff --git a/src/Monolog/Handler/AbstractSyslogHandler.php b/src/Monolog/Handler/AbstractSyslogHandler.php index 49e7da5b..5f3de1fb 100644 --- a/src/Monolog/Handler/AbstractSyslogHandler.php +++ b/src/Monolog/Handler/AbstractSyslogHandler.php @@ -22,23 +22,6 @@ abstract class AbstractSyslogHandler extends AbstractProcessingHandler { protected int $facility; - /** - * Translates Monolog log levels to syslog log priorities. - */ - protected function toSyslogPriority(Level $level): int - { - return match ($level) { - Level::Debug => \LOG_DEBUG, - Level::Info => \LOG_INFO, - Level::Notice => \LOG_NOTICE, - Level::Warning => \LOG_WARNING, - Level::Error => \LOG_ERR, - Level::Critical => \LOG_CRIT, - Level::Alert => \LOG_ALERT, - Level::Emergency => \LOG_EMERG, - }; - } - /** * List of valid log facility names. * @var array diff --git a/src/Monolog/Handler/SyslogUdp/UdpSocket.php b/src/Monolog/Handler/Syslog/SyslogUdp/UdpSocket.php similarity index 97% rename from src/Monolog/Handler/SyslogUdp/UdpSocket.php rename to src/Monolog/Handler/Syslog/SyslogUdp/UdpSocket.php index 6a483345..a599753d 100644 --- a/src/Monolog/Handler/SyslogUdp/UdpSocket.php +++ b/src/Monolog/Handler/Syslog/SyslogUdp/UdpSocket.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Monolog\Handler\SyslogUdp; +namespace Monolog\Handler\Syslog\SyslogUdp; use Monolog\Utils; use Socket; diff --git a/src/Monolog/Handler/Syslog/SyslogUtils.php b/src/Monolog/Handler/Syslog/SyslogUtils.php new file mode 100644 index 00000000..61981ba4 --- /dev/null +++ b/src/Monolog/Handler/Syslog/SyslogUtils.php @@ -0,0 +1,22 @@ + \LOG_DEBUG, + Level::Info => \LOG_INFO, + Level::Notice => \LOG_NOTICE, + Level::Warning => \LOG_WARNING, + Level::Error => \LOG_ERR, + Level::Critical => \LOG_CRIT, + Level::Alert => \LOG_ALERT, + Level::Emergency => \LOG_EMERG, + }; + } +} diff --git a/src/Monolog/Handler/SyslogHandler.php b/src/Monolog/Handler/SyslogHandler.php index 0816a011..63779c4d 100644 --- a/src/Monolog/Handler/SyslogHandler.php +++ b/src/Monolog/Handler/SyslogHandler.php @@ -11,6 +11,7 @@ namespace Monolog\Handler; +use Monolog\Handler\Syslog\SyslogUtils; use Monolog\Level; use Monolog\Utils; use Monolog\LogRecord; @@ -61,6 +62,6 @@ class SyslogHandler extends AbstractSyslogHandler if (!openlog($this->ident, $this->logopts, $this->facility)) { throw new \LogicException('Can\'t open syslog for ident "'.$this->ident.'" and facility "'.$this->facility.'"' . Utils::getRecordMessageForException($record)); } - syslog($this->toSyslogPriority($record->level), (string) $record->formatted); + syslog(SyslogUtils::toSyslogPriority($record->level), (string) $record->formatted); } } diff --git a/src/Monolog/Handler/SyslogUdpHandler.php b/src/Monolog/Handler/SyslogUdpHandler.php index 3d30fbd4..209b3b40 100644 --- a/src/Monolog/Handler/SyslogUdpHandler.php +++ b/src/Monolog/Handler/SyslogUdpHandler.php @@ -13,7 +13,8 @@ namespace Monolog\Handler; use DateTimeInterface; use Monolog\Level; -use Monolog\Handler\SyslogUdp\UdpSocket; +use Monolog\Handler\Syslog\SyslogUdp\UdpSocket; +use Monolog\Handler\Syslog\SyslogUtils; use Monolog\Utils; use Monolog\LogRecord; @@ -70,7 +71,7 @@ class SyslogUdpHandler extends AbstractSyslogHandler { $lines = $this->splitMessageIntoLines($record->formatted); - $header = $this->makeCommonSyslogHeader($this->toSyslogPriority($record->level), $record->datetime); + $header = $this->makeCommonSyslogHeader(SyslogUtils::toSyslogPriority($record->level), $record->datetime); foreach ($lines as $line) { $this->socket->write($line, $header); diff --git a/tests/Monolog/Formatter/SyslogFormatterTest.php b/tests/Monolog/Formatter/SyslogFormatterTest.php new file mode 100644 index 00000000..fe8933ea --- /dev/null +++ b/tests/Monolog/Formatter/SyslogFormatterTest.php @@ -0,0 +1,113 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Formatter; + +use DateTimeImmutable; +use Monolog\Level; +use Monolog\LogRecord; +use PHPUnit\Framework\TestCase; + +class SyslogFormatterTest extends TestCase +{ + /** + * @dataProvider formatDataProvider + * + * @param string $expected + * @param DateTimeImmutable $dateTime + * @param string $channel + * @param Level $level + * @param string $message + * @param string|null $appName + * @param mixed[] $context + * @param mixed[] $extra + * @return void + */ + public function testFormat( + string $expected, + DateTimeImmutable $dateTime, + string $channel, + Level $level, + string $message, + string $appName = null, + array $context = [], + array $extra = [] + ): void { + if ($appName !== null) { + $formatter = new SyslogFormatter($appName); + } else { + $formatter = new SyslogFormatter(); + } + + $record = new LogRecord( + datetime: $dateTime, + channel: $channel, + level: $level, + message: $message, + context: $context, + extra: $extra + ); + + $message = $formatter->format($record); + + $this->assertEquals($expected, $message); + } + + /** + * @return mixed[] + */ + public function formatDataProvider(): array + { + return [ + 'error' => [ + 'expected' => "<11>1 1970-01-01T00:00:00.000000+00:00 " . gethostname() . " - " . getmypid() ." meh - ERROR: log \n", + 'dateTime' => new DateTimeImmutable("@0"), + 'channel' => 'meh', + 'level' => Level::Error, + 'message' => 'log', + ], + 'info' => [ + 'expected' => "<11>1 1970-01-01T00:00:00.000000+00:00 " . gethostname() . " - " . getmypid() ." meh - ERROR: log \n", + 'dateTime' => new DateTimeImmutable("@0"), + 'channel' => 'meh', + 'level' => Level::Error, + 'message' => 'log', + ], + 'with app name' => [ + 'expected' => "<11>1 1970-01-01T00:00:00.000000+00:00 " . gethostname() . " my-app " . getmypid() ." meh - ERROR: log \n", + 'dateTime' => new DateTimeImmutable("@0"), + 'channel' => 'meh', + 'level' => Level::Error, + 'message' => 'log', + 'appName' => 'my-app', + ], + 'with context' => [ + 'expected' => "<11>1 1970-01-01T00:00:00.000000+00:00 " . gethostname() . " - " . getmypid() ." meh - ERROR: log {\"additional-context\":\"test\"} \n", + 'dateTime' => new DateTimeImmutable("@0"), + 'channel' => 'meh', + 'level' => Level::Error, + 'message' => 'log', + 'appName' => null, + 'context' => ['additional-context' => 'test'], + ], + 'with extra' => [ + 'expected' => "<11>1 1970-01-01T00:00:00.000000+00:00 " . gethostname() . " - " . getmypid() ." meh - ERROR: log {\"userId\":1}\n", + 'dateTime' => new DateTimeImmutable("@0"), + 'channel' => 'meh', + 'level' => Level::Error, + 'message' => 'log', + 'appName' => null, + 'context' => [], + 'extra' => ['userId' => 1], + ], + ]; + } +} diff --git a/tests/Monolog/Handler/SyslogUdpHandlerTest.php b/tests/Monolog/Handler/SyslogUdpHandlerTest.php index f27d5ad5..786b758c 100644 --- a/tests/Monolog/Handler/SyslogUdpHandlerTest.php +++ b/tests/Monolog/Handler/SyslogUdpHandlerTest.php @@ -35,7 +35,7 @@ class SyslogUdpHandlerTest extends TestCase $handler->setFormatter(new \Monolog\Formatter\ChromePHPFormatter()); $time = '2014-01-07T12:34:56+00:00'; - $socket = $this->getMockBuilder('Monolog\Handler\SyslogUdp\UdpSocket') + $socket = $this->getMockBuilder('Monolog\Handler\Syslog\SyslogUdp\UdpSocket') ->onlyMethods(['write']) ->setConstructorArgs(['lol']) ->getMock(); @@ -56,7 +56,7 @@ class SyslogUdpHandlerTest extends TestCase $handler = new SyslogUdpHandler("127.0.0.1", 514, "authpriv"); $handler->setFormatter($this->getIdentityFormatter()); - $socket = $this->getMockBuilder('Monolog\Handler\SyslogUdp\UdpSocket') + $socket = $this->getMockBuilder('Monolog\Handler\Syslog\SyslogUdp\UdpSocket') ->onlyMethods(['write']) ->setConstructorArgs(['lol']) ->getMock(); @@ -81,7 +81,7 @@ class SyslogUdpHandlerTest extends TestCase $handler->setFormatter(new \Monolog\Formatter\ChromePHPFormatter()); - $socket = $this->getMockBuilder('\Monolog\Handler\SyslogUdp\UdpSocket') + $socket = $this->getMockBuilder('\Monolog\Handler\Syslog\SyslogUdp\UdpSocket') ->setConstructorArgs(['lol', 999]) ->onlyMethods(['write']) ->getMock(); diff --git a/tests/Monolog/Handler/UdpSocketTest.php b/tests/Monolog/Handler/UdpSocketTest.php index 58477997..d394361b 100644 --- a/tests/Monolog/Handler/UdpSocketTest.php +++ b/tests/Monolog/Handler/UdpSocketTest.php @@ -12,7 +12,7 @@ namespace Monolog\Handler; use Monolog\Test\TestCase; -use Monolog\Handler\SyslogUdp\UdpSocket; +use Monolog\Handler\Syslog\SyslogUdp\UdpSocket; /** * @requires extension sockets @@ -21,7 +21,7 @@ class UdpSocketTest extends TestCase { public function testWeDoNotTruncateShortMessages() { - $socket = $this->getMockBuilder('Monolog\Handler\SyslogUdp\UdpSocket') + $socket = $this->getMockBuilder('Monolog\Handler\Syslog\SyslogUdp\UdpSocket') ->onlyMethods(['send']) ->setConstructorArgs(['lol']) ->getMock(); @@ -35,7 +35,7 @@ class UdpSocketTest extends TestCase public function testLongMessagesAreTruncated() { - $socket = $this->getMockBuilder('Monolog\Handler\SyslogUdp\UdpSocket') + $socket = $this->getMockBuilder('Monolog\Handler\Syslog\SyslogUdp\UdpSocket') ->onlyMethods(['send']) ->setConstructorArgs(['lol']) ->getMock();