Skip to content

Commit

Permalink
feature #21003 [Console][FrameworkBundle] Log console exceptions (jam…
Browse files Browse the repository at this point in the history
…eshalsall, chalasr)

This PR was merged into the 3.3-dev branch.

Discussion
----------

[Console][FrameworkBundle] Log console exceptions

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

Continues #19382, fixing some issues including:
- ability to display the input string for any `InputInterface` implementation (cast to string if possible, use the command name otherwise)
- if the input can be casted as string, cleanup the result (from `command "'command:name' --foo=bar" ` to `command "command:name --foo=bar"`)
- made `ExceptionLister::$logger` private instead of protected
-  changed methods name from `onKernel*` to `onConsole*` (e.g. `onConsoleException`) and removed unnecessary doc blocks
- Added more tests

Log for an exception:

> [2016-12-22 00:34:42] app.ERROR: Exception thrown while running command: "cache:clear -vvv". Message: "An error occured!" {"exception":"[object] (RuntimeException(code: 0): An error occured! at /Volumes/HD/Sites/tests/sf-demo-3.2/vendor/symfony/symfony/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php:61)","command":"cache:clear -vvv","message":"An error occured!"} []

Commits
-------

919041c Add Console ExceptionListener
9896547 Add basic support for automatic console exception logging
  • Loading branch information
fabpot committed Jan 23, 2017
2 parents 4e66554 + 919041c commit c2a6ddc
Show file tree
Hide file tree
Showing 6 changed files with 229 additions and 1 deletion.
Expand Up @@ -28,6 +28,7 @@
use Symfony\Component\Finder\Finder;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\Config\Resource\ClassExistenceResource;
use Symfony\Component\PropertyAccess\PropertyAccessor;
use Symfony\Component\Serializer\Encoder\YamlEncoder;
use Symfony\Component\Serializer\Encoder\CsvEncoder;
Expand All @@ -38,6 +39,7 @@
use Symfony\Component\Workflow;
use Symfony\Component\Workflow\SupportStrategy\ClassInstanceSupportStrategy;
use Symfony\Component\Yaml\Yaml;
use Symfony\Component\Console\Application;

/**
* FrameworkExtension.
Expand Down Expand Up @@ -83,6 +85,11 @@ public function load(array $configs, ContainerBuilder $container)

$loader->load('fragment_renderer.xml');

$container->addResource(new ClassExistenceResource(Application::class));
if (class_exists(Application::class)) {
$loader->load('console.xml');
}

// Property access is used by both the Form and the Validator component
$loader->load('property_access.xml');

Expand Down
16 changes: 16 additions & 0 deletions src/Symfony/Bundle/FrameworkBundle/Resources/config/console.xml
@@ -0,0 +1,16 @@
<?xml version="1.0" ?>

<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

<services>

<service id="console.exception_listener" class="Symfony\Component\Console\EventListener\ExceptionListener" public="false">
<argument type="service" id="logger" on-invalid="null" />
<tag name="kernel.event_subscriber" />
<tag name="monolog.logger" channel="console" />
</service>

</services>
</container>
2 changes: 1 addition & 1 deletion src/Symfony/Bundle/FrameworkBundle/composer.json
Expand Up @@ -21,7 +21,7 @@
"symfony/class-loader": "~3.2",
"symfony/dependency-injection": "~3.3",
"symfony/config": "~3.3",
"symfony/event-dispatcher": "~2.8|~3.0",
"symfony/event-dispatcher": "~3.3",
"symfony/http-foundation": "~3.1",
"symfony/http-kernel": "~3.3",
"symfony/polyfill-mbstring": "~1.0",
Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Component/Console/CHANGELOG.md
Expand Up @@ -4,6 +4,7 @@ CHANGELOG
3.3.0
-----

* added `ExceptionListener`
* added `AddConsoleCommandPass` (originally in FrameworkBundle)

3.2.0
Expand Down
79 changes: 79 additions & 0 deletions src/Symfony/Component/Console/EventListener/ExceptionListener.php
@@ -0,0 +1,79 @@
<?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\Component\Console\EventListener;

use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Event\ConsoleEvent;
use Symfony\Component\Console\ConsoleEvents;
use Symfony\Component\Console\Event\ConsoleExceptionEvent;
use Symfony\Component\Console\Event\ConsoleTerminateEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

/**
* @author James Halsall <james.t.halsall@googlemail.com>
* @author Robin Chalas <robin.chalas@gmail.com>
*/
class ExceptionListener implements EventSubscriberInterface
{
private $logger;

public function __construct(LoggerInterface $logger = null)
{
$this->logger = $logger;
}

public function onConsoleException(ConsoleExceptionEvent $event)
{
if (null === $this->logger) {
return;
}

$exception = $event->getException();

$this->logger->error('Exception thrown while running command "{command}". Message: "{message}"', array('exception' => $exception, 'command' => $this->getInputString($event), 'message' => $exception->getMessage()));
}

public function onConsoleTerminate(ConsoleTerminateEvent $event)
{
if (null === $this->logger) {
return;
}

$exitCode = $event->getExitCode();

if (0 === $exitCode) {
return;
}

$this->logger->error('Command "{command}" exited with code "{code}"', array('command' => $this->getInputString($event), 'code' => $exitCode));
}

public static function getSubscribedEvents()
{
return array(
ConsoleEvents::EXCEPTION => array('onConsoleException', -128),
ConsoleEvents::TERMINATE => array('onConsoleTerminate', -128),
);
}

private static function getInputString(ConsoleEvent $event)
{
$commandName = $event->getCommand()->getName();
$input = $event->getInput();

if (method_exists($input, '__toString')) {
return str_replace(array("'$commandName'", "\"$commandName\""), $commandName, (string) $input);
}

return $commandName;
}
}
@@ -0,0 +1,125 @@
<?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\Component\Console\Tests\EventListener;

use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Event\ConsoleExceptionEvent;
use Symfony\Component\Console\Event\ConsoleTerminateEvent;
use Symfony\Component\Console\EventListener\ExceptionListener;
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\StringInput;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class ExceptionListenerTest extends \PHPUnit_Framework_TestCase
{
public function testOnConsoleException()
{
$exception = new \RuntimeException('An error occurred');

$logger = $this->getLogger();
$logger
->expects($this->once())
->method('error')
->with('Exception thrown while running command "{command}". Message: "{message}"', array('exception' => $exception, 'command' => 'test:run --foo=baz buzz', 'message' => 'An error occurred'))
;

$listener = new ExceptionListener($logger);
$listener->onConsoleException($this->getConsoleExceptionEvent($exception, new ArgvInput(array('console.php', 'test:run', '--foo=baz', 'buzz')), 1));
}

public function testOnConsoleTerminateForNonZeroExitCodeWritesToLog()
{
$logger = $this->getLogger();
$logger
->expects($this->once())
->method('error')
->with('Command "{command}" exited with code "{code}"', array('command' => 'test:run', 'code' => 255))
;

$listener = new ExceptionListener($logger);
$listener->onConsoleTerminate($this->getConsoleTerminateEvent(new ArgvInput(array('console.php', 'test:run')), 255));
}

public function testOnConsoleTerminateForZeroExitCodeDoesNotWriteToLog()
{
$logger = $this->getLogger();
$logger
->expects($this->never())
->method('error')
;

$listener = new ExceptionListener($logger);
$listener->onConsoleTerminate($this->getConsoleTerminateEvent(new ArgvInput(array('console.php', 'test:run')), 0));
}

public function testGetSubscribedEvents()
{
$this->assertEquals(
array(
'console.exception' => array('onConsoleException', -128),
'console.terminate' => array('onConsoleTerminate', -128),
),
ExceptionListener::getSubscribedEvents()
);
}

public function testAllKindsOfInputCanBeLogged()
{
$logger = $this->getLogger();
$logger
->expects($this->exactly(3))
->method('error')
->with('Command "{command}" exited with code "{code}"', array('command' => 'test:run --foo=bar', 'code' => 255))
;

$listener = new ExceptionListener($logger);
$listener->onConsoleTerminate($this->getConsoleTerminateEvent(new ArgvInput(array('console.php', 'test:run', '--foo=bar')), 255));
$listener->onConsoleTerminate($this->getConsoleTerminateEvent(new ArrayInput(array('name' => 'test:run', '--foo' => 'bar')), 255));
$listener->onConsoleTerminate($this->getConsoleTerminateEvent(new StringInput('test:run --foo=bar'), 255));
}

public function testCommandNameIsDisplayedForNonStringableInput()
{
$logger = $this->getLogger();
$logger
->expects($this->once())
->method('error')
->with('Command "{command}" exited with code "{code}"', array('command' => 'test:run', 'code' => 255))
;

$listener = new ExceptionListener($logger);
$listener->onConsoleTerminate($this->getConsoleTerminateEvent($this->getMockBuilder(InputInterface::class)->getMock(), 255));
}

private function getLogger()
{
return $this->getMockForAbstractClass(LoggerInterface::class);
}

private function getConsoleExceptionEvent(\Exception $exception, InputInterface $input, $exitCode)
{
return new ConsoleExceptionEvent(new Command('test:run'), $input, $this->getOutput(), $exception, $exitCode);
}

private function getConsoleTerminateEvent(InputInterface $input, $exitCode)
{
return new ConsoleTerminateEvent(new Command('test:run'), $input, $this->getOutput(), $exitCode);
}

private function getOutput()
{
return $this->getMockBuilder(OutputInterface::class)->getMock();
}
}

0 comments on commit c2a6ddc

Please sign in to comment.