diff --git a/src/Symfony/Bridge/Monolog/CHANGELOG.md b/src/Symfony/Bridge/Monolog/CHANGELOG.md index 2cb6c3f39d..8b519c9f31 100644 --- a/src/Symfony/Bridge/Monolog/CHANGELOG.md +++ b/src/Symfony/Bridge/Monolog/CHANGELOG.md @@ -1,6 +1,12 @@ CHANGELOG ========= +4.3.0 +----- + + * added `ConsoleCommandProcessor`: monolog processor that adds command name and arguments + * added `RouteProcessor`: monolog processor that adds route name, controller::action and route params + 4.2.0 ----- diff --git a/src/Symfony/Bridge/Monolog/Processor/ConsoleCommandProcessor.php b/src/Symfony/Bridge/Monolog/Processor/ConsoleCommandProcessor.php new file mode 100644 index 0000000000..1fb8cc1ef3 --- /dev/null +++ b/src/Symfony/Bridge/Monolog/Processor/ConsoleCommandProcessor.php @@ -0,0 +1,69 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Monolog\Processor; + +use Symfony\Component\Console\ConsoleEvents; +use Symfony\Component\Console\Event\ConsoleEvent; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Contracts\Service\ResetInterface; + +/** + * Adds the current console command information to the log entry. + * + * @author Piotr Stankowski + */ +class ConsoleCommandProcessor implements EventSubscriberInterface, ResetInterface +{ + private $commandData; + private $includeArguments; + private $includeOptions; + + public function __construct(bool $includeArguments = true, bool $includeOptions = false) + { + $this->includeArguments = $includeArguments; + $this->includeOptions = $includeOptions; + } + + public function __invoke(array $records) + { + if (null !== $this->commandData && !isset($records['extra']['command'])) { + $records['extra']['command'] = $this->commandData; + } + + return $records; + } + + public function reset() + { + $this->commandData = null; + } + + public function addCommandData(ConsoleEvent $event) + { + $this->commandData = array( + 'name' => $event->getCommand()->getName(), + ); + if ($this->includeArguments) { + $this->commandData['arguments'] = $event->getInput()->getArguments(); + } + if ($this->includeOptions) { + $this->commandData['options'] = $event->getInput()->getOptions(); + } + } + + public static function getSubscribedEvents() + { + return array( + ConsoleEvents::COMMAND => array('addCommandData', 1), + ); + } +} diff --git a/src/Symfony/Bridge/Monolog/Processor/RouteProcessor.php b/src/Symfony/Bridge/Monolog/Processor/RouteProcessor.php new file mode 100644 index 0000000000..b00c97079b --- /dev/null +++ b/src/Symfony/Bridge/Monolog/Processor/RouteProcessor.php @@ -0,0 +1,86 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Monolog\Processor; + +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpKernel\Event\FinishRequestEvent; +use Symfony\Component\HttpKernel\Event\GetResponseEvent; +use Symfony\Component\HttpKernel\KernelEvents; +use Symfony\Contracts\Service\ResetInterface; + +/** + * Adds the current route information to the log entry. + * + * @author Piotr Stankowski + */ +class RouteProcessor implements EventSubscriberInterface, ResetInterface +{ + private $routeData; + private $includeParams; + + public function __construct(bool $includeParams = true) + { + $this->includeParams = $includeParams; + $this->reset(); + } + + public function __invoke(array $records) + { + if ($this->routeData && !isset($records['extra']['requests'])) { + $records['extra']['requests'] = array_values($this->routeData); + } + + return $records; + } + + public function reset() + { + $this->routeData = array(); + } + + public function addRouteData(GetResponseEvent $event) + { + if ($event->isMasterRequest()) { + $this->reset(); + } + + $request = $event->getRequest(); + if (!$request->attributes->has('_controller')) { + return; + } + + $currentRequestData = array( + 'controller' => $request->attributes->get('_controller'), + 'route' => $request->attributes->get('_route'), + ); + + if ($this->includeParams) { + $currentRequestData['route_params'] = $request->attributes->get('_route_params'); + } + + $this->routeData[spl_object_id($request)] = $currentRequestData; + } + + public function removeRouteData(FinishRequestEvent $event) + { + $requestId = spl_object_id($event->getRequest()); + unset($this->routeData[$requestId]); + } + + public static function getSubscribedEvents() + { + return array( + KernelEvents::REQUEST => array('addRouteData', 1), + KernelEvents::FINISH_REQUEST => array('removeRouteData', 1), + ); + } +} diff --git a/src/Symfony/Bridge/Monolog/Tests/Processor/ConsoleCommandProcessorTest.php b/src/Symfony/Bridge/Monolog/Tests/Processor/ConsoleCommandProcessorTest.php new file mode 100644 index 0000000000..2709c6a395 --- /dev/null +++ b/src/Symfony/Bridge/Monolog/Tests/Processor/ConsoleCommandProcessorTest.php @@ -0,0 +1,75 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Monolog\Tests\Processor; + +use PHPUnit\Framework\TestCase; +use Symfony\Bridge\Monolog\Processor\ConsoleCommandProcessor; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Event\ConsoleEvent; +use Symfony\Component\Console\Input\InputInterface; + +class ConsoleCommandProcessorTest extends TestCase +{ + private const TEST_ARGUMENTS = array('test' => 'argument'); + private const TEST_OPTIONS = array('test' => 'option'); + private const TEST_NAME = 'some:test'; + + public function testProcessor() + { + $processor = new ConsoleCommandProcessor(); + $processor->addCommandData($this->getConsoleEvent()); + + $record = $processor(array('extra' => array())); + + $this->assertArrayHasKey('command', $record['extra']); + $this->assertEquals( + array('name' => self::TEST_NAME, 'arguments' => self::TEST_ARGUMENTS), + $record['extra']['command'] + ); + } + + public function testProcessorWithOptions() + { + $processor = new ConsoleCommandProcessor(true, true); + $processor->addCommandData($this->getConsoleEvent()); + + $record = $processor(array('extra' => array())); + + $this->assertArrayHasKey('command', $record['extra']); + $this->assertEquals( + array('name' => self::TEST_NAME, 'arguments' => self::TEST_ARGUMENTS, 'options' => self::TEST_OPTIONS), + $record['extra']['command'] + ); + } + + public function testProcessorDoesNothingWhenNotInConsole() + { + $processor = new ConsoleCommandProcessor(true, true); + + $record = $processor(array('extra' => array())); + $this->assertEquals(array('extra' => array()), $record); + } + + private function getConsoleEvent(): ConsoleEvent + { + $input = $this->getMockBuilder(InputInterface::class)->getMock(); + $input->method('getArguments')->willReturn(self::TEST_ARGUMENTS); + $input->method('getOptions')->willReturn(self::TEST_OPTIONS); + $command = $this->getMockBuilder(Command::class)->disableOriginalConstructor()->getMock(); + $command->method('getName')->willReturn(self::TEST_NAME); + $consoleEvent = $this->getMockBuilder(ConsoleEvent::class)->disableOriginalConstructor()->getMock(); + $consoleEvent->method('getCommand')->willReturn($command); + $consoleEvent->method('getInput')->willReturn($input); + + return $consoleEvent; + } +} diff --git a/src/Symfony/Bridge/Monolog/Tests/Processor/RouteProcessorTest.php b/src/Symfony/Bridge/Monolog/Tests/Processor/RouteProcessorTest.php new file mode 100644 index 0000000000..2a9c065886 --- /dev/null +++ b/src/Symfony/Bridge/Monolog/Tests/Processor/RouteProcessorTest.php @@ -0,0 +1,162 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Monolog\Tests\Processor; + +use PHPUnit\Framework\TestCase; +use Symfony\Bridge\Monolog\Processor\RouteProcessor; +use Symfony\Component\HttpFoundation\ParameterBag; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Event\FinishRequestEvent; +use Symfony\Component\HttpKernel\Event\GetResponseEvent; + +class RouteProcessorTest extends TestCase +{ + private const TEST_CONTROLLER = 'App\Controller\SomeController::someMethod'; + private const TEST_ROUTE = 'someRouteName'; + private const TEST_PARAMS = array('param1' => 'value1'); + + public function testProcessor() + { + $request = $this->mockFilledRequest(); + $processor = new RouteProcessor(); + $processor->addRouteData($this->mockGetResponseEvent($request)); + + $record = $processor(array('extra' => array())); + + $this->assertArrayHasKey('requests', $record['extra']); + $this->assertCount(1, $record['extra']['requests']); + $this->assertEquals( + array('controller' => self::TEST_CONTROLLER, 'route' => self::TEST_ROUTE, 'route_params' => self::TEST_PARAMS), + $record['extra']['requests'][0] + ); + } + + public function testProcessorWithoutParams() + { + $request = $this->mockFilledRequest(); + $processor = new RouteProcessor(false); + $processor->addRouteData($this->mockGetResponseEvent($request)); + + $record = $processor(array('extra' => array())); + + $this->assertArrayHasKey('requests', $record['extra']); + $this->assertCount(1, $record['extra']['requests']); + $this->assertEquals( + array('controller' => self::TEST_CONTROLLER, 'route' => self::TEST_ROUTE), + $record['extra']['requests'][0] + ); + } + + public function testProcessorWithSubRequests() + { + $controllerFromSubRequest = 'OtherController::otherMethod'; + $mainRequest = $this->mockFilledRequest(); + $subRequest = $this->mockFilledRequest($controllerFromSubRequest); + + $processor = new RouteProcessor(false); + $processor->addRouteData($this->mockGetResponseEvent($mainRequest)); + $processor->addRouteData($this->mockGetResponseEvent($subRequest)); + + $record = $processor(array('extra' => array())); + + $this->assertArrayHasKey('requests', $record['extra']); + $this->assertCount(2, $record['extra']['requests']); + $this->assertEquals( + array('controller' => self::TEST_CONTROLLER, 'route' => self::TEST_ROUTE), + $record['extra']['requests'][0] + ); + $this->assertEquals( + array('controller' => $controllerFromSubRequest, 'route' => self::TEST_ROUTE), + $record['extra']['requests'][1] + ); + } + + public function testFinishRequestRemovesRelatedEntry() + { + $mainRequest = $this->mockFilledRequest(); + $subRequest = $this->mockFilledRequest('OtherController::otherMethod'); + + $processor = new RouteProcessor(false); + $processor->addRouteData($this->mockGetResponseEvent($mainRequest)); + $processor->addRouteData($this->mockGetResponseEvent($subRequest)); + $processor->removeRouteData($this->mockFinishRequestEvent($subRequest)); + $record = $processor(array('extra' => array())); + + $this->assertArrayHasKey('requests', $record['extra']); + $this->assertCount(1, $record['extra']['requests']); + $this->assertEquals( + array('controller' => self::TEST_CONTROLLER, 'route' => self::TEST_ROUTE), + $record['extra']['requests'][0] + ); + + $processor->removeRouteData($this->mockFinishRequestEvent($mainRequest)); + $record = $processor(array('extra' => array())); + + $this->assertArrayNotHasKey('requests', $record['extra']); + } + + public function testProcessorWithEmptyRequest() + { + $request = $this->mockEmptyRequest(); + $processor = new RouteProcessor(); + $processor->addRouteData($this->mockGetResponseEvent($request)); + + $record = $processor(array('extra' => array())); + $this->assertEquals(array('extra' => array()), $record); + } + + public function testProcessorDoesNothingWhenNoRequest() + { + $processor = new RouteProcessor(); + + $record = $processor(array('extra' => array())); + $this->assertEquals(array('extra' => array()), $record); + } + + private function mockGetResponseEvent(Request $request): GetResponseEvent + { + $event = $this->getMockBuilder(GetResponseEvent::class)->disableOriginalConstructor()->getMock(); + $event->method('getRequest')->willReturn($request); + + return $event; + } + + private function mockFinishRequestEvent(Request $request): FinishRequestEvent + { + $event = $this->getMockBuilder(FinishRequestEvent::class)->disableOriginalConstructor()->getMock(); + $event->method('getRequest')->willReturn($request); + + return $event; + } + + private function mockEmptyRequest(): Request + { + return $this->mockRequest(array()); + } + + private function mockFilledRequest(string $controller = self::TEST_CONTROLLER): Request + { + return $this->mockRequest(array( + '_controller' => $controller, + '_route' => self::TEST_ROUTE, + '_route_params' => self::TEST_PARAMS, + )); + } + + private function mockRequest(array $attributes): Request + { + $request = $this->getMockBuilder(Request::class)->disableOriginalConstructor()->getMock(); + $request->attributes = new ParameterBag($attributes); + + return $request; + } +}