From 1e446b8b36c1c3c2247f6d7cb5a14be5bd68233c Mon Sep 17 00:00:00 2001 From: Ron Rise Date: Thu, 13 Nov 2025 14:09:27 -0500 Subject: [PATCH] chore: add deployment configurations and tests for logger and dispatcher --- .run/ Compose Deployment.run.xml | 10 ++ .run/All.run.xml | 8 ++ .run/Lint_fix.run.xml | 8 ++ .run/Main.run.xml | 11 ++ docker-compose.yml | 2 +- src/Log/Logger.php | 29 ++-- src/Services/Facades/RoadRunnerLogger.php | 39 +++++ tests/Facades/DispatcherTest.php | 20 +++ tests/Facades/LoggerTest.php | 20 +++ tests/Helpers/UlidTest.php | 19 +++ tests/Log/LoggerRpcTest.php | 167 ++++++++++++++++++++++ tests/Log/LoggerTest.php | 155 ++++++++++++++++++++ 12 files changed, 477 insertions(+), 11 deletions(-) create mode 100644 .run/ Compose Deployment.run.xml create mode 100644 .run/All.run.xml create mode 100644 .run/Lint_fix.run.xml create mode 100644 .run/Main.run.xml create mode 100644 src/Services/Facades/RoadRunnerLogger.php create mode 100644 tests/Facades/DispatcherTest.php create mode 100644 tests/Facades/LoggerTest.php create mode 100644 tests/Helpers/UlidTest.php create mode 100644 tests/Log/LoggerRpcTest.php create mode 100644 tests/Log/LoggerTest.php diff --git a/.run/ Compose Deployment.run.xml b/.run/ Compose Deployment.run.xml new file mode 100644 index 0000000..46b86bd --- /dev/null +++ b/.run/ Compose Deployment.run.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/.run/All.run.xml b/.run/All.run.xml new file mode 100644 index 0000000..77c7e80 --- /dev/null +++ b/.run/All.run.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/.run/Lint_fix.run.xml b/.run/Lint_fix.run.xml new file mode 100644 index 0000000..06b7491 --- /dev/null +++ b/.run/Lint_fix.run.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/.run/Main.run.xml b/.run/Main.run.xml new file mode 100644 index 0000000..99253aa --- /dev/null +++ b/.run/Main.run.xml @@ -0,0 +1,11 @@ + + + + + + + + + + \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 587c7b9..2a5bbd0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -83,7 +83,7 @@ services: postgres: condition: service_healthy environment: - QUEUE_BROKER: kafka + QUEUE_BROKER: redis PHP_IDE_CONFIG: serverName=localhost WORKERS: 1 DEBUG: 1 diff --git a/src/Log/Logger.php b/src/Log/Logger.php index 00c0a25..69127dc 100644 --- a/src/Log/Logger.php +++ b/src/Log/Logger.php @@ -6,10 +6,12 @@ namespace Siteworxpro\App\Log; use Monolog\Formatter\JsonFormatter; use Monolog\Handler\StreamHandler; +use Psr\Container\ContainerExceptionInterface; +use Psr\Container\NotFoundExceptionInterface; use Psr\Log\LoggerInterface; use Psr\Log\LogLevel; use RoadRunner\Logger\Logger as RRLogger; -use Spiral\Goridge\RPC\RPC; +use Siteworxpro\App\Services\Facades\RoadRunnerLogger; /** * Logger implementation that conforms to PSR-3 (`Psr\Log\LoggerInterface`). @@ -30,9 +32,9 @@ class Logger implements LoggerInterface /** * RoadRunner RPC logger instance when running under RoadRunner. * - * @var RRLogger|null + * @var RRLogger | LoggerInterface | null */ - private ?RRLogger $rpcLogger = null; + private RRLogger | LoggerInterface | null $rpcLogger = null; /** * Monolog logger used as a fallback to write JSON-formatted logs to stdout. @@ -66,21 +68,28 @@ class Logger implements LoggerInterface * @param string $level Minimum level to log (PSR-3 level string). Messages with * a higher numeric value in `$levels` will be ignored. * + * @param resource | null $streamOutput Optional stream handler for Monolog. + * * The default is `LogLevel::DEBUG` (log everything). * * If `$_SERVER['RR_RPC']` is set, an RPC connection will be attempted at * $_SERVER['RR_RPC'] and a RoadRunner RPC logger will be used. + * + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface */ - public function __construct(private readonly string $level = LogLevel::DEBUG) - { + public function __construct( + private readonly string $level = LogLevel::DEBUG, + $streamOutput = null, + ) { if (isset($_SERVER['RR_RPC'])) { - $rpc = RPC::create($_SERVER['RR_RPC']); - $this->rpcLogger = new RRLogger($rpc); + $this->rpcLogger = RoadRunnerLogger::getFacadeRoot(); } $this->monologLogger = new \Monolog\Logger('app_logger'); $formatter = new JsonFormatter(); - $this->monologLogger->pushHandler(new StreamHandler('php://stdout')->setFormatter($formatter)); + $stream = $streamOutput ?? 'php://stdout'; + $this->monologLogger->pushHandler(new StreamHandler($stream)->setFormatter($formatter)); } /** @@ -192,7 +201,7 @@ class Logger implements LoggerInterface */ public function log($level, \Stringable|string $message, array $context = []): void { - if ($this->levels[$level] > $this->levels[$this->level]) { + if (isset($this->levels[$level]) && $this->levels[$level] > $this->levels[$this->level]) { return; } @@ -215,7 +224,7 @@ class Logger implements LoggerInterface $this->rpcLogger->error((string)$message, $context); break; default: - $this->rpcLogger->log((string)$message, $context); + $this->rpcLogger->log($level, (string)$message, $context); break; } diff --git a/src/Services/Facades/RoadRunnerLogger.php b/src/Services/Facades/RoadRunnerLogger.php new file mode 100644 index 0000000..044f414 --- /dev/null +++ b/src/Services/Facades/RoadRunnerLogger.php @@ -0,0 +1,39 @@ +has(Logger::class) === false) { + $rpc = RPC::create($_SERVER['RR_RPC']); + $logger = new Logger($rpc); + $container->bind(static::getFacadeAccessor(), function () use ($logger) { + return $logger; + }); + + return $logger; + } + + return $container->get(Logger::class); + } + + protected static function getFacadeAccessor(): string + { + return Logger::class; + } +} diff --git a/tests/Facades/DispatcherTest.php b/tests/Facades/DispatcherTest.php new file mode 100644 index 0000000..ade0fd9 --- /dev/null +++ b/tests/Facades/DispatcherTest.php @@ -0,0 +1,20 @@ +assertIsString($ulid); + $this->assertEquals(16, strlen($ulid)); + } +} diff --git a/tests/Log/LoggerRpcTest.php b/tests/Log/LoggerRpcTest.php new file mode 100644 index 0000000..329c96b --- /dev/null +++ b/tests/Log/LoggerRpcTest.php @@ -0,0 +1,167 @@ +expectNotToPerformAssertions(); + + $_SERVER['RR_RPC'] = 'tcp://127.0.0.1:6001'; + + $mock = Mockery::mock(LoggerInterface::class); + $mock->expects('debug') + ->with('message', ['key' => 'value']) + ->once(); + + \Siteworxpro\App\Services\Facades\Logger::getFacadeContainer() + ->bind(\RoadRunner\Logger\Logger::class, function () use ($mock) { + return $mock; + }); + + $inputBuffer = fopen('php://memory', 'r+'); + $logger = new Logger(LogLevel::DEBUG, $inputBuffer); + $logger->debug('message', ['key' => 'value']); + + $mock->shouldHaveReceived('debug'); + + Mockery::close(); + } + + /** + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + public function testLogsDebugMessageWhenLevelIsInfoNotice(): void + { + $this->expectNotToPerformAssertions(); + + $_SERVER['RR_RPC'] = 'tcp://127.0.0.1:6001'; + + $mock = Mockery::mock(LoggerInterface::class); + $mock->expects('info') + ->with('message', ['key' => 'value']) + ->times(2); + + \Siteworxpro\App\Services\Facades\Logger::getFacadeContainer() + ->bind(\RoadRunner\Logger\Logger::class, function () use ($mock) { + return $mock; + }); + + $inputBuffer = fopen('php://memory', 'r+'); + $logger = new Logger(LogLevel::DEBUG, $inputBuffer); + $logger->info('message', ['key' => 'value']); + $logger->notice('message', ['key' => 'value']); + + $mock->shouldHaveReceived('info')->times(2); + Mockery::close(); + } + + /** + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + public function testLogsDebugMessageWhenLevelIsInfoWarning(): void + { + $this->expectNotToPerformAssertions(); + + $_SERVER['RR_RPC'] = 'tcp://127.0.0.1:6001'; + + $mock = Mockery::mock(LoggerInterface::class); + $mock->expects('warning') + ->with('message', ['key' => 'value']) + ->times(1); + + \Siteworxpro\App\Services\Facades\Logger::getFacadeContainer() + ->bind(\RoadRunner\Logger\Logger::class, function () use ($mock) { + return $mock; + }); + + $inputBuffer = fopen('php://memory', 'r+'); + $logger = new Logger(LogLevel::DEBUG, $inputBuffer); + $logger->warning('message', ['key' => 'value']); + + $mock->shouldHaveReceived('warning'); + Mockery::close(); + } + + /** + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + public function testLogsDebugMessageWhenLevelIsInfoError(): void + { + $this->expectNotToPerformAssertions(); + + $_SERVER['RR_RPC'] = 'tcp://127.0.0.1:6001'; + + $mock = Mockery::mock(LoggerInterface::class); + $mock->expects('error') + ->with('message', ['key' => 'value']) + ->times(4); + + \Siteworxpro\App\Services\Facades\Logger::getFacadeContainer() + ->bind(\RoadRunner\Logger\Logger::class, function () use ($mock) { + return $mock; + }); + + $inputBuffer = fopen('php://memory', 'r+'); + $logger = new Logger(LogLevel::DEBUG, $inputBuffer); + $logger->error('message', ['key' => 'value']); + $logger->critical('message', ['key' => 'value']); + $logger->alert('message', ['key' => 'value']); + $logger->emergency('message', ['key' => 'value']); + + $mock->shouldHaveReceived('error')->times(4); + Mockery::close(); + } + + /** + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + public function testLogsLog(): void + { + $this->expectNotToPerformAssertions(); + + $_SERVER['RR_RPC'] = 'tcp://127.0.0.1:6001'; + + $mock = Mockery::mock(LoggerInterface::class); + $mock->expects('log') + ->with('notaloglevel', 'message', ['key' => 'value']); + + \Siteworxpro\App\Services\Facades\Logger::getFacadeContainer() + ->bind(\RoadRunner\Logger\Logger::class, function () use ($mock) { + return $mock; + }); + + $inputBuffer = fopen('php://memory', 'r+'); + $logger = new Logger(LogLevel::DEBUG, $inputBuffer); + $logger->log('notaloglevel', 'message', ['key' => 'value']); + + $mock->shouldHaveReceived('log')->times(1); + Mockery::close(); + } +} diff --git a/tests/Log/LoggerTest.php b/tests/Log/LoggerTest.php new file mode 100644 index 0000000..4de291b --- /dev/null +++ b/tests/Log/LoggerTest.php @@ -0,0 +1,155 @@ +getLoggerWithBuffer($level); + $logger->$level('message', ['key' => 'value']); + $output = $this->getContents($inputBuffer); + + $this->assertNotEmpty($output); + $decoded = json_decode($output, true); + $this->assertEquals('message', $decoded['message']); + $this->assertEquals('value', $decoded['context']['key']); + } + + private function testLogLevelEmpty(string $configLevel, string $logLevel): void + { + [$logger, $inputBuffer] = $this->getLoggerWithBuffer($configLevel); + $logger->$logLevel('message', ['key' => 'value']); + $output = $this->getContents($inputBuffer); + + $this->assertEmpty($output); + } + + public function testLogsDebugMessageWhenLevelIsDebug(): void + { + $this->testLogLevel(LogLevel::DEBUG); + } + + public function testLogsInfoMessageWhenLevelIsInfo(): void + { + $this->testLogLevel(LogLevel::INFO); + } + + public function testLogsWarningMessageWhenLevelIsWarning(): void + { + $this->testLogLevel(LogLevel::WARNING); + } + + public function testLogsErrorMessageWhenLevelIsError(): void + { + $this->testLogLevel(LogLevel::ERROR); + } + + public function testLogsCriticalMessageWhenLevelIsCritical(): void + { + $this->testLogLevel(LogLevel::CRITICAL); + } + + public function testLogsAlertMessageWhenLevelIsAlert(): void + { + $this->testLogLevel(LogLevel::ALERT); + } + + public function testLogsEmergencyMessageWhenLevelIsEmergency(): void + { + $this->testLogLevel(LogLevel::EMERGENCY); + } + + public function testLogsNoticeMessageWhenLevelIsNotice(): void + { + $this->testLogLevel(LogLevel::NOTICE); + } + + public function testDoesNotLogWhenMinimumLevelIsInfo(): void + { + $this->testLogLevelEmpty(LogLevel::INFO, LogLevel::DEBUG); + } + + public function testDoesNotLogWhenMinimumLevelIsWarning(): void + { + $this->testLogLevelEmpty(LogLevel::WARNING, LogLevel::INFO); + $this->testLogLevelEmpty(LogLevel::WARNING, LogLevel::DEBUG); + } + + public function testDoesNotLogWhenMinimumLevelIsError(): void + { + $this->testLogLevelEmpty(LogLevel::ERROR, LogLevel::DEBUG); + $this->testLogLevelEmpty(LogLevel::ERROR, LogLevel::INFO); + $this->testLogLevelEmpty(LogLevel::ERROR, LogLevel::WARNING); + } + + public function testDoesNotLogWhenMinimumLevelIsNotice(): void + { + $this->testLogLevelEmpty(LogLevel::NOTICE, LogLevel::DEBUG); + $this->testLogLevelEmpty(LogLevel::NOTICE, LogLevel::INFO); + } + + public function testLogsMessageWithEmptyContext(): void + { + [$logger, $buffer] = $this->getLoggerWithBuffer(LogLevel::INFO); + + $logger->info('Message without context'); + $output = $this->getContents($buffer); + + $this->assertNotEmpty($output); + $decoded = json_decode($output, true); + $this->assertEquals('Message without context', $decoded['message']); + } + + public function testLogsMessageWithComplexContext(): void + { + [$logger, $buffer] = $this->getLoggerWithBuffer(LogLevel::INFO); + + $logger->info('Complex context', [ + 'user_id' => 123, + 'nested' => ['key' => 'value'], + 'array' => [1, 2, 3] + ]); + $output = $this->getContents($buffer); + + $this->assertNotEmpty($output); + $decoded = json_decode($output, true); + $this->assertEquals(123, $decoded['context']['user_id']); + $this->assertEquals('value', $decoded['context']['nested']['key']); + } + + public function testLogsStringableMessage(): void + { + [$logger, $buffer] = $this->getLoggerWithBuffer(LogLevel::INFO); + + $stringable = new class implements \Stringable { + public function __toString(): string + { + return 'Stringable message'; + } + }; + + $logger->info($stringable); + $output = $this->getContents($buffer); + $this->assertNotEmpty($output); + $decoded = json_decode($output, true); + $this->assertEquals('Stringable message', $decoded['message']); + } +}