feature #28330 [MonologBridge] Add monolog processors adding route and command info (trakos)

This PR was squashed before being merged into the 4.3-dev branch (closes #28330).

Discussion
----------

[MonologBridge] Add monolog processors adding route and command info

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets |
| License       | MIT
| Doc PR        | symfony/symfony-docs#10244

This PR adds two simple processors that add context to every log entry.

RouteProcessor adds routing information:
`app.INFO: Some log text {"someContext":"ctx"} {"route":{"controller":"App\\Controller\\SomeController::number","route":"index","route_params":[]}`

ConsoleCommandProcessors adds current command information:
`app.INFO: Some log text {"someContext":"ctx"} {"command":{"name":"app:some-command","arguments":{"command":"app:some-command","some-argument":10}}}`

For ConsoleCommandProcessor I've decided against including command options by default, because there's a lot of default ones:
`"options":{"help":false,"quiet":false,"verbose":false,"version":false,"ansi":false,"no-ansi":false,"no-interaction":false,"env":"dev","no-debug":false}`. This behavior can be changed with a constructor argument.

Commits
-------

669f6b2726 [MonologBridge] Add monolog processors adding route and command info
This commit is contained in:
Fabien Potencier 2019-03-17 08:08:51 +01:00
commit 6fa4d2b0cf
5 changed files with 398 additions and 0 deletions

View File

@ -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
-----

View File

@ -0,0 +1,69 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <git@trakos.pl>
*/
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),
);
}
}

View File

@ -0,0 +1,86 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <git@trakos.pl>
*/
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),
);
}
}

View File

@ -0,0 +1,75 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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;
}
}

View File

@ -0,0 +1,162 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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;
}
}