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:
commit
6fa4d2b0cf
@ -1,6 +1,12 @@
|
|||||||
CHANGELOG
|
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
|
4.2.0
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
@ -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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
86
src/Symfony/Bridge/Monolog/Processor/RouteProcessor.php
Normal file
86
src/Symfony/Bridge/Monolog/Processor/RouteProcessor.php
Normal 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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user