Skip to content

Commit c2a6ddc

Browse files
committed
feature symfony#21003 [Console][FrameworkBundle] Log console exceptions (jameshalsall, 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 | symfony#10895 | License | MIT | Doc PR | symfony/symfony-docs#7373 Continues symfony#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
2 parents 4e66554 + 919041c commit c2a6ddc

File tree

6 files changed

+229
-1
lines changed

6 files changed

+229
-1
lines changed

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
use Symfony\Component\Finder\Finder;
2929
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
3030
use Symfony\Component\Config\FileLocator;
31+
use Symfony\Component\Config\Resource\ClassExistenceResource;
3132
use Symfony\Component\PropertyAccess\PropertyAccessor;
3233
use Symfony\Component\Serializer\Encoder\YamlEncoder;
3334
use Symfony\Component\Serializer\Encoder\CsvEncoder;
@@ -38,6 +39,7 @@
3839
use Symfony\Component\Workflow;
3940
use Symfony\Component\Workflow\SupportStrategy\ClassInstanceSupportStrategy;
4041
use Symfony\Component\Yaml\Yaml;
42+
use Symfony\Component\Console\Application;
4143

4244
/**
4345
* FrameworkExtension.
@@ -83,6 +85,11 @@ public function load(array $configs, ContainerBuilder $container)
8385

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

88+
$container->addResource(new ClassExistenceResource(Application::class));
89+
if (class_exists(Application::class)) {
90+
$loader->load('console.xml');
91+
}
92+
8693
// Property access is used by both the Form and the Validator component
8794
$loader->load('property_access.xml');
8895

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?xml version="1.0" ?>
2+
3+
<container xmlns="http://symfony.com/schema/dic/services"
4+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
5+
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
6+
7+
<services>
8+
9+
<service id="console.exception_listener" class="Symfony\Component\Console\EventListener\ExceptionListener" public="false">
10+
<argument type="service" id="logger" on-invalid="null" />
11+
<tag name="kernel.event_subscriber" />
12+
<tag name="monolog.logger" channel="console" />
13+
</service>
14+
15+
</services>
16+
</container>

src/Symfony/Bundle/FrameworkBundle/composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
"symfony/class-loader": "~3.2",
2222
"symfony/dependency-injection": "~3.3",
2323
"symfony/config": "~3.3",
24-
"symfony/event-dispatcher": "~2.8|~3.0",
24+
"symfony/event-dispatcher": "~3.3",
2525
"symfony/http-foundation": "~3.1",
2626
"symfony/http-kernel": "~3.3",
2727
"symfony/polyfill-mbstring": "~1.0",

src/Symfony/Component/Console/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ CHANGELOG
44
3.3.0
55
-----
66

7+
* added `ExceptionListener`
78
* added `AddConsoleCommandPass` (originally in FrameworkBundle)
89

910
3.2.0
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Console\EventListener;
13+
14+
use Psr\Log\LoggerInterface;
15+
use Symfony\Component\Console\Event\ConsoleEvent;
16+
use Symfony\Component\Console\ConsoleEvents;
17+
use Symfony\Component\Console\Event\ConsoleExceptionEvent;
18+
use Symfony\Component\Console\Event\ConsoleTerminateEvent;
19+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
20+
21+
/**
22+
* @author James Halsall <james.t.halsall@googlemail.com>
23+
* @author Robin Chalas <robin.chalas@gmail.com>
24+
*/
25+
class ExceptionListener implements EventSubscriberInterface
26+
{
27+
private $logger;
28+
29+
public function __construct(LoggerInterface $logger = null)
30+
{
31+
$this->logger = $logger;
32+
}
33+
34+
public function onConsoleException(ConsoleExceptionEvent $event)
35+
{
36+
if (null === $this->logger) {
37+
return;
38+
}
39+
40+
$exception = $event->getException();
41+
42+
$this->logger->error('Exception thrown while running command "{command}". Message: "{message}"', array('exception' => $exception, 'command' => $this->getInputString($event), 'message' => $exception->getMessage()));
43+
}
44+
45+
public function onConsoleTerminate(ConsoleTerminateEvent $event)
46+
{
47+
if (null === $this->logger) {
48+
return;
49+
}
50+
51+
$exitCode = $event->getExitCode();
52+
53+
if (0 === $exitCode) {
54+
return;
55+
}
56+
57+
$this->logger->error('Command "{command}" exited with code "{code}"', array('command' => $this->getInputString($event), 'code' => $exitCode));
58+
}
59+
60+
public static function getSubscribedEvents()
61+
{
62+
return array(
63+
ConsoleEvents::EXCEPTION => array('onConsoleException', -128),
64+
ConsoleEvents::TERMINATE => array('onConsoleTerminate', -128),
65+
);
66+
}
67+
68+
private static function getInputString(ConsoleEvent $event)
69+
{
70+
$commandName = $event->getCommand()->getName();
71+
$input = $event->getInput();
72+
73+
if (method_exists($input, '__toString')) {
74+
return str_replace(array("'$commandName'", "\"$commandName\""), $commandName, (string) $input);
75+
}
76+
77+
return $commandName;
78+
}
79+
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Console\Tests\EventListener;
13+
14+
use Psr\Log\LoggerInterface;
15+
use Symfony\Component\Console\Command\Command;
16+
use Symfony\Component\Console\Event\ConsoleExceptionEvent;
17+
use Symfony\Component\Console\Event\ConsoleTerminateEvent;
18+
use Symfony\Component\Console\EventListener\ExceptionListener;
19+
use Symfony\Component\Console\Input\ArgvInput;
20+
use Symfony\Component\Console\Input\ArrayInput;
21+
use Symfony\Component\Console\Input\StringInput;
22+
use Symfony\Component\Console\Input\InputInterface;
23+
use Symfony\Component\Console\Output\OutputInterface;
24+
25+
class ExceptionListenerTest extends \PHPUnit_Framework_TestCase
26+
{
27+
public function testOnConsoleException()
28+
{
29+
$exception = new \RuntimeException('An error occurred');
30+
31+
$logger = $this->getLogger();
32+
$logger
33+
->expects($this->once())
34+
->method('error')
35+
->with('Exception thrown while running command "{command}". Message: "{message}"', array('exception' => $exception, 'command' => 'test:run --foo=baz buzz', 'message' => 'An error occurred'))
36+
;
37+
38+
$listener = new ExceptionListener($logger);
39+
$listener->onConsoleException($this->getConsoleExceptionEvent($exception, new ArgvInput(array('console.php', 'test:run', '--foo=baz', 'buzz')), 1));
40+
}
41+
42+
public function testOnConsoleTerminateForNonZeroExitCodeWritesToLog()
43+
{
44+
$logger = $this->getLogger();
45+
$logger
46+
->expects($this->once())
47+
->method('error')
48+
->with('Command "{command}" exited with code "{code}"', array('command' => 'test:run', 'code' => 255))
49+
;
50+
51+
$listener = new ExceptionListener($logger);
52+
$listener->onConsoleTerminate($this->getConsoleTerminateEvent(new ArgvInput(array('console.php', 'test:run')), 255));
53+
}
54+
55+
public function testOnConsoleTerminateForZeroExitCodeDoesNotWriteToLog()
56+
{
57+
$logger = $this->getLogger();
58+
$logger
59+
->expects($this->never())
60+
->method('error')
61+
;
62+
63+
$listener = new ExceptionListener($logger);
64+
$listener->onConsoleTerminate($this->getConsoleTerminateEvent(new ArgvInput(array('console.php', 'test:run')), 0));
65+
}
66+
67+
public function testGetSubscribedEvents()
68+
{
69+
$this->assertEquals(
70+
array(
71+
'console.exception' => array('onConsoleException', -128),
72+
'console.terminate' => array('onConsoleTerminate', -128),
73+
),
74+
ExceptionListener::getSubscribedEvents()
75+
);
76+
}
77+
78+
public function testAllKindsOfInputCanBeLogged()
79+
{
80+
$logger = $this->getLogger();
81+
$logger
82+
->expects($this->exactly(3))
83+
->method('error')
84+
->with('Command "{command}" exited with code "{code}"', array('command' => 'test:run --foo=bar', 'code' => 255))
85+
;
86+
87+
$listener = new ExceptionListener($logger);
88+
$listener->onConsoleTerminate($this->getConsoleTerminateEvent(new ArgvInput(array('console.php', 'test:run', '--foo=bar')), 255));
89+
$listener->onConsoleTerminate($this->getConsoleTerminateEvent(new ArrayInput(array('name' => 'test:run', '--foo' => 'bar')), 255));
90+
$listener->onConsoleTerminate($this->getConsoleTerminateEvent(new StringInput('test:run --foo=bar'), 255));
91+
}
92+
93+
public function testCommandNameIsDisplayedForNonStringableInput()
94+
{
95+
$logger = $this->getLogger();
96+
$logger
97+
->expects($this->once())
98+
->method('error')
99+
->with('Command "{command}" exited with code "{code}"', array('command' => 'test:run', 'code' => 255))
100+
;
101+
102+
$listener = new ExceptionListener($logger);
103+
$listener->onConsoleTerminate($this->getConsoleTerminateEvent($this->getMockBuilder(InputInterface::class)->getMock(), 255));
104+
}
105+
106+
private function getLogger()
107+
{
108+
return $this->getMockForAbstractClass(LoggerInterface::class);
109+
}
110+
111+
private function getConsoleExceptionEvent(\Exception $exception, InputInterface $input, $exitCode)
112+
{
113+
return new ConsoleExceptionEvent(new Command('test:run'), $input, $this->getOutput(), $exception, $exitCode);
114+
}
115+
116+
private function getConsoleTerminateEvent(InputInterface $input, $exitCode)
117+
{
118+
return new ConsoleTerminateEvent(new Command('test:run'), $input, $this->getOutput(), $exitCode);
119+
}
120+
121+
private function getOutput()
122+
{
123+
return $this->getMockBuilder(OutputInterface::class)->getMock();
124+
}
125+
}

0 commit comments

Comments
 (0)