diff --git a/DependencyInjection/Compiler/RequestIdentifierPass.php b/DependencyInjection/Compiler/RequestIdentifierPass.php new file mode 100644 index 0000000..0e6bd9b --- /dev/null +++ b/DependencyInjection/Compiler/RequestIdentifierPass.php @@ -0,0 +1,28 @@ + + */ +class RequestIdentifierPass implements CompilerPassInterface +{ + + /** + * @inheritdoc + */ + public function process(ContainerBuilder $container) + { + if (!$container->hasParameter('onelog.enable_request_id') || true === $container->getParameter('onelog.enable_request_id')) { + return; + } + + $container->removeDefinition(RequestIdProcessor::class); + } +} diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 96525d3..1cbdbc0 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -40,6 +40,11 @@ public function getConfigTreeBuilder() ->defaultFalse() ->treatNullLike(false) ->end() + ->booleanNode('enable_request_id') + ->info('Add a request identifier to all log entries. Allows for easier tracking of logs during a request') + ->defaultTrue() + ->treatNullLike(true) + ->end() ->end(); return $treeBuilder; diff --git a/DependencyInjection/OnelogExtension.php b/DependencyInjection/OnelogExtension.php index 3cecad4..7d75332 100644 --- a/DependencyInjection/OnelogExtension.php +++ b/DependencyInjection/OnelogExtension.php @@ -28,5 +28,6 @@ public function load(array $configs, ContainerBuilder $container) $container->setParameter('onelog.logger_service', $config['logger_service']); $container->setParameter('onelog.register_global', $config['register_global']); $container->setParameter('onelog.register_monolog_channels', $config['register_monolog_channels']); + $container->setParameter('onelog.enable_request_id', $config['enable_request_id']); } } diff --git a/Helper/OneLogStatic.php b/Helper/OneLogStatic.php index b593406..d9d2bf9 100644 --- a/Helper/OneLogStatic.php +++ b/Helper/OneLogStatic.php @@ -65,10 +65,6 @@ public static function destroy() */ public static function __callStatic(string $level, $params) { - if (!static::$resolved instanceof OneLog) { - throw new \RuntimeException('Logger is not properly instantiated!'); - } - - return self::$resolved->{$level}(...$params); + return self::instance()->{$level}(...$params); } } diff --git a/Monolog/RequestIdProcessor.php b/Monolog/RequestIdProcessor.php new file mode 100644 index 0000000..c31d158 --- /dev/null +++ b/Monolog/RequestIdProcessor.php @@ -0,0 +1,50 @@ + + */ +class RequestIdProcessor +{ + + /** + * @var int + */ + private $logCount = 1; + + /** + * @var RequestId + */ + private $requestId; + + /** + * RequestIdProcessor constructor. + * + * @param IdentifierInterface $requestId + */ + public function __construct(IdentifierInterface $requestId = null) + { + $this->requestId = $requestId ?? RequestId::generate(); + } + + /** + * Add the request id to the log entry + * + * @param array $record + * + * @return array + */ + public function __invoke(array $record): array + { + $record['extra']['request_id'] = $this->requestId->identifier() . '.' . $this->logCount; + $this->logCount++; + + return $record; + } +} \ No newline at end of file diff --git a/OnelogBundle.php b/OnelogBundle.php index 46dd781..f43a7f2 100644 --- a/OnelogBundle.php +++ b/OnelogBundle.php @@ -4,6 +4,7 @@ use KoderHut\OnelogBundle\DependencyInjection\Compiler\LoggerWrapPass; use KoderHut\OnelogBundle\DependencyInjection\Compiler\RegisterMonologChannels; +use KoderHut\OnelogBundle\DependencyInjection\Compiler\RequestIdentifierPass; use KoderHut\OnelogBundle\Helper\GlobalNamespaceRegister; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Bundle\Bundle; @@ -29,6 +30,7 @@ public function build(ContainerBuilder $container) { $container->addCompilerPass(new LoggerWrapPass()); $container->addCompilerPass(new RegisterMonologChannels()); + $container->addCompilerPass(new RequestIdentifierPass()); } } diff --git a/PSRLoggerTrait.php b/PSRLoggerTrait.php index b2ee0e6..7b58ab1 100644 --- a/PSRLoggerTrait.php +++ b/PSRLoggerTrait.php @@ -9,11 +9,12 @@ */ trait PSRLoggerTrait { + /** * @param mixed $message * @param array $context */ - public function emergency($message, array $context = array()) + public function emergency($message, array $context = []) { $this->defaultLogger->emergency($message, $context); } @@ -22,7 +23,7 @@ public function emergency($message, array $context = array()) * @param mixed $message * @param array $context */ - public function alert($message, array $context = array()) + public function alert($message, array $context = []) { $this->defaultLogger->alert($message, $context); } @@ -31,7 +32,7 @@ public function alert($message, array $context = array()) * @param mixed $message * @param array $context */ - public function critical($message, array $context = array()) + public function critical($message, array $context = []) { $this->defaultLogger->critical($message, $context); } @@ -40,7 +41,7 @@ public function critical($message, array $context = array()) * @param mixed $message * @param array $context */ - public function error($message, array $context = array()) + public function error($message, array $context = []) { $this->defaultLogger->error($message, $context); } @@ -49,7 +50,7 @@ public function error($message, array $context = array()) * @param mixed $message * @param array $context */ - public function warning($message, array $context = array()) + public function warning($message, array $context = []) { $this->defaultLogger->warning($message, $context); } @@ -58,7 +59,7 @@ public function warning($message, array $context = array()) * @param mixed $message * @param array $context */ - public function notice($message, array $context = array()) + public function notice($message, array $context = []) { $this->defaultLogger->notice($message, $context); } @@ -67,7 +68,7 @@ public function notice($message, array $context = array()) * @param mixed $message * @param array $context */ - public function info($message, array $context = array()) + public function info($message, array $context = []) { $this->defaultLogger->info($message, $context); } @@ -76,7 +77,7 @@ public function info($message, array $context = array()) * @param mixed $message * @param array $context */ - public function debug($message, array $context = array()) + public function debug($message, array $context = []) { $this->defaultLogger->debug($message, $context); } @@ -86,8 +87,8 @@ public function debug($message, array $context = array()) * @param mixed $message * @param array $context */ - public function log($level, $message, array $context = array()) + public function log($level, $message, array $context = []) { - return $this->defaultLogger->log($level, $message, $context); + $this->defaultLogger->log($level, $message, $context); } } diff --git a/Request/Identifier.php b/Request/Identifier.php new file mode 100644 index 0000000..fb1266a --- /dev/null +++ b/Request/Identifier.php @@ -0,0 +1,55 @@ + + */ +class Identifier implements IdentifierInterface +{ + + /** + * @var string + */ + private $identifier; + + /** + * Identifier constructor. + * + * @param string $identifier + */ + public function __construct(string $identifier) + { + $this->identifier = $identifier; + } + + /** + * It will generate an instance based on the current date + * using the format YmdHis.u + * + * @param string ...$salts + * + * @return Identifier + */ + public static function generate(string ...$salts) + { + $hashData = (string) (new \DateTime())->format('Ymd.His.u'); + + if (!empty($salts)) { + $hashData .= '.' . implode('.', $salts); + } + + return new self($hashData); + } + + /** + * @inheritdoc + */ + public function identifier(): string + { + return $this->identifier; + } + +} diff --git a/Request/IdentifierInterface.php b/Request/IdentifierInterface.php new file mode 100644 index 0000000..610944d --- /dev/null +++ b/Request/IdentifierInterface.php @@ -0,0 +1,19 @@ + + */ +interface IdentifierInterface +{ + + /** + * Return the current identifier + * + * @return string + */ + public function identifier(): string; +} diff --git a/Resources/config/onelog.xml b/Resources/config/onelog.xml index 63a7a01..c4a3c0c 100644 --- a/Resources/config/onelog.xml +++ b/Resources/config/onelog.xml @@ -11,6 +11,10 @@ + + + + diff --git a/Tests/DependencyInjection/Compiler/RequestIdentifierPassTest.php b/Tests/DependencyInjection/Compiler/RequestIdentifierPassTest.php new file mode 100644 index 0000000..7994a55 --- /dev/null +++ b/Tests/DependencyInjection/Compiler/RequestIdentifierPassTest.php @@ -0,0 +1,69 @@ +prophesize(ContainerBuilder::class); + $container->hasParameter('onelog.enable_request_id') + ->shouldBeCalled() + ->willReturn(false) + ; + + $instance = new RequestIdentifierPass(); + + $instance->process($container->reveal()); + } + + /** + * @test + */ + public function testExitEarlyIfConfigParamIsMissing() + { + $container = $this->prophesize(ContainerBuilder::class); + $container->hasParameter('onelog.enable_request_id') + ->shouldBeCalled() + ->willReturn(true) + ; + $container->getParameter('onelog.enable_request_id') + ->shouldBeCalled() + ->willReturn(true) + ; + + $instance = new RequestIdentifierPass(); + + $instance->process($container->reveal()); + } + + /** + * @test + */ + public function testRemoveRequestIdServiceIfConfigIsSetToFalse() + { + $container = $this->prophesize(ContainerBuilder::class); + $container->hasParameter('onelog.enable_request_id') + ->shouldBeCalled() + ->willReturn(true) + ; + $container->getParameter('onelog.enable_request_id') + ->shouldBeCalled() + ->willReturn(false) + ; + $container->removeDefinition(RequestIdProcessor::class)->shouldBeCalled(); + + $instance = new RequestIdentifierPass(); + + $instance->process($container->reveal()); + } +} diff --git a/Tests/DependencyInjection/ConfigurationTest.php b/Tests/DependencyInjection/ConfigurationTest.php index 928cd44..91d7612 100644 --- a/Tests/DependencyInjection/ConfigurationTest.php +++ b/Tests/DependencyInjection/ConfigurationTest.php @@ -34,12 +34,26 @@ public function optionsProvider() { return [ 'all_configs' => [ - ['onelog' => ['logger_service' => 'monolog', 'register_global' => true, 'register_monolog_channels' => false]], - ['logger_service' => 'monolog', 'register_global' => true, 'register_monolog_channels' => false], + ['onelog' => [ + 'logger_service' => 'monolog', + 'register_global' => true, + 'register_monolog_channels' => false, + ]], + [ + 'logger_service' => 'monolog', + 'register_global' => true, + 'register_monolog_channels' => false, + 'enable_request_id' => true, + ], ], 'default_configs' => [ ['onelog' => ['logger_service' => null]], - ['logger_service' => 'logger', 'register_global' => false, 'register_monolog_channels' => false], + [ + 'logger_service' => 'logger', + 'register_global' => false, + 'register_monolog_channels' => false, + 'enable_request_id' => true, + ], ], ]; } diff --git a/Tests/Helper/OneLogStaticTest.php b/Tests/Helper/OneLogStaticTest.php new file mode 100644 index 0000000..c6d51bc --- /dev/null +++ b/Tests/Helper/OneLogStaticTest.php @@ -0,0 +1,65 @@ +expectException(\RuntimeException::class); + $this->expectExceptionMessage('OneLog is not properly instantiated!'); + + OneLogStatic::instance(); + } + + /** + * @test + */ + public function testCallingPsrLoggerMethodsAreProxiedToLoggerInstance() + { + $logger = $this->prophesize(OneLog::class); + $logger->debug('test', ['test'])->shouldBeCalled(); + + OneLogStatic::setInstance($logger->reveal()); + OneLogStatic::debug('test', ['test']); + } + + /** + * @test + */ + public function testDestroyClearsLoggerInstance() + { + $logger = $this->prophesize(OneLog::class); + + OneLogStatic::setInstance($logger->reveal()); + + $this->assertSame($logger->reveal(), OneLogStatic::instance()); + + OneLogStatic::destroy(); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('OneLog is not properly instantiated!'); + + OneLogStatic::instance(); + } + + /** + * @test + */ + public function testInstanceCannotBeCreated() + { + $this->expectException(\Error::class); + $this->expectExceptionMessage("Call to private KoderHut\OnelogBundle\Helper\OneLogStatic::__construct() from context 'KoderHut\OneLogBundle\Tests\Helper\OneLogStaticTest'"); + + $instance = new OneLogStatic(); + } +} diff --git a/Tests/Monolog/RequestIdProcessorTest.php b/Tests/Monolog/RequestIdProcessorTest.php new file mode 100644 index 0000000..b5a667d --- /dev/null +++ b/Tests/Monolog/RequestIdProcessorTest.php @@ -0,0 +1,65 @@ +format('Ymd'); + $time = $dateTime->format('Hi'); + $format = "${date}\.${time}[0-5]{1}[0-9]{1}\.[0-9]{6}\.1"; + + $instance = new RequestIdProcessor(); + + $result = $instance([]); + + $this->assertArrayHasKey('extra', $result); + $this->assertArrayHasKey('request_id', $result['extra']); + $this->assertRegExp("/${format}/", $result['extra']['request_id']); + } + + /** + * @test + */ + public function testInstanceAcceptSpecificIdentifier() + { + $identifier = $this->prophesize(IdentifierInterface::class); + $identifier->identifier()->shouldBeCalled()->willReturn('test'); + $instance = new RequestIdProcessor($identifier->reveal()); + + $result = $instance([]); + $this->assertArrayHasKey('extra', $result); + $this->assertArrayHasKey('request_id', $result['extra']); + $this->assertEquals('test.1', $result['extra']['request_id']); + } + + /** + * @test + */ + public function testMultipleLogEntriesWillBeMarkedIncrementally() + { + $identifier = $this->prophesize(IdentifierInterface::class); + $identifier->identifier()->shouldBeCalled()->willReturn('test'); + $instance = new RequestIdProcessor($identifier->reveal()); + + $result = $instance([]); + $this->assertArrayHasKey('extra', $result); + $this->assertArrayHasKey('request_id', $result['extra']); + $this->assertEquals('test.1', $result['extra']['request_id']); + + $result = $instance([]); + $this->assertArrayHasKey('extra', $result); + $this->assertArrayHasKey('request_id', $result['extra']); + $this->assertEquals('test.2', $result['extra']['request_id']); + } +} diff --git a/Tests/PSRLoggerTraitTest.php b/Tests/PSRLoggerTraitTest.php new file mode 100644 index 0000000..120090b --- /dev/null +++ b/Tests/PSRLoggerTraitTest.php @@ -0,0 +1,88 @@ +mockLogger = $this->prophesize(LoggerInterface::class); + $this->instance = new MockPSRLoggerTrait($this->mockLogger->reveal()); + } + + /** + * @test + * + * @dataProvider loggerMethods + * + * @param string $method + */ + public function testMethodsAreDispatchedToLogger(string $method) + { + $this->mockLogger->{$method}('test', [])->shouldBeCalled(); + + $this->instance->{$method}('test', []); + } + + /** + * @test + */ + public function testCallingLogIsProxiedToLogger() + { + $this->mockLogger->log('alert', 'test', [])->shouldBeCalled(); + + $this->instance->log('alert', 'test', []); + } + /** + * @see testMethodsAreDispatchedToLogger + * + * @return array + */ + public function loggerMethods() + { + return [ + 'emergency' => ['emergency'], + 'alert' => ['alert'], + 'critical' => ['critical'], + 'error' => ['error'], + 'warning' => ['warning'], + 'notice' => ['notice'], + 'info' => ['info'], + 'debug' => ['debug'], + ]; + } +} + +/** + * Class MockPSRLoggerTrait + * + * @author Denis-Florin Rendler + */ +class MockPSRLoggerTrait +{ + use PSRLoggerTrait; + + protected $defaultLogger; + + public function __construct($logger) + { + $this->defaultLogger = $logger; + } +} diff --git a/Tests/Request/IdentifierTest.php b/Tests/Request/IdentifierTest.php new file mode 100644 index 0000000..d980b16 --- /dev/null +++ b/Tests/Request/IdentifierTest.php @@ -0,0 +1,56 @@ + + */ +class IdentifierTest extends TestCase +{ + + /** + * @test + */ + public function testCanProvideSpecificIdentifierString() + { + $instance = new Identifier('test'); + + $this->assertEquals('test', $instance->identifier()); + } + + /** + * @test + */ + public function testGeneratingInstanceFromStaticCall() + { + $dateTime = new \DateTime(); + $date = $dateTime->format('Ymd'); + $time = $dateTime->format('Hi'); + $format = "${date}\.${time}[0-5]{1}[0-9]{1}\.[0-9]{6}"; + $instance = Identifier::generate(); + + $this->assertInstanceOf(IdentifierInterface::class, $instance); + $this->assertRegExp("/${format}/", $instance->identifier()); + } + + /** + * @test + */ + public function testAddingSaltsToIdentifier() + { + $dateTime = new \DateTime(); + $date = $dateTime->format('Ymd'); + $time = $dateTime->format('Hi'); + $format = "${date}\.${time}[0-5]{1}[0-9]{1}\.[0-9]{6}\.test1\.test2"; + $instance = Identifier::generate('test1', 'test2'); + + $this->assertInstanceOf(IdentifierInterface::class, $instance); + $this->assertRegExp("/${format}/", $instance->identifier()); + } +}