Skip to content

Cookbook article on how to add exceptions logging in console commands #1965

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Dec 24, 2012
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cookbook/console/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ Console
console_command
usage
generating_urls
logging
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's also a map.rst.inc file in the cookbook/ directory that needs a similar change

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

245 changes: 245 additions & 0 deletions cookbook/console/logging.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
.. index::
single: Console; Enabling logging

How to enable logging in Console Commands
=========================================

The Console component doesn't provide any logging capabilities out of the box.
Normally, you run console commands manually and observe the output, that's
why logging is not provided. However, there are cases when you might need
logging. For example, if you are running console commands unattended, such
as from cron jobs or deployment scripts it may be easier to use Symfony's
logging capabilities instead of configuring other tools to gather console
output and process it. This can be especially handful if you already have
some existing setup for aggregating and analyzing Symfony logs.

There are basically two logging cases you would need:
* Manually logging some information from your command;
* Logging uncaught Exceptions.

Manually logging from console command
-------------------------------------

This one is really simple. When you create console command within full framewok
as described :doc:`here</cookbook/console/console_command>`, your command
extends :class:`Symfony\\Bundle\\FrameworkBundle\\Command\\ContainerAwareCommand`,
so you can simply access standard logger service through the container and
use it to do the logging::

// src/Acme/DemoBundle/Command/GreetCommand.php
namespace Acme\DemoBundle\Command;

use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\HttpKernel\Log\LoggerInterface;

class GreetCommand extends ContainerAwareCommand
{
// ...

protected function execute(InputInterface $input, OutputInterface $output)
{
/** @var $logger LoggerInterface */
$logger = $this->getContainer()->get('logger');

$name = $input->getArgument('name');
if ($name) {
$text = 'Hello '.$name;
} else {
$text = 'Hello';
}

if ($input->getOption('yell')) {
$text = strtoupper($text);
$logger->warn('Yelled: '.$text);
}
else {
$logger->info('Greeted: '.$text);
}

$output->writeln($text);
}
}

Depending on the environment you run your command you will get the results
in ``app/logs/dev.log`` or ``app/logs/prod.log``.

Enabling automatic Exceptions logging
-------------------------------------

In order to enable console application to automatically log uncaught exceptions
for all commands you'd need to do something more.

First, you have to extend :class:`Symfony\\Bundle\\FrameworkBundle\\Console\\Application`
class to override its :method:`Symfony\\Bundle\\FrameworkBundle\\Console\\Application::run`
method, where exception handling should happen::

// src/Acme/DemoBundle/Console/Application.php
namespace Acme\DemoBundle\Console;

use Symfony\Bundle\FrameworkBundle\Console\Application as BaseApplication;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\HttpKernel\Log\LoggerInterface;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\Console\Output\ConsoleOutput;
use Symfony\Component\Console\Input\ArgvInput;

class Application extends BaseApplication
{
private $originalAutoExit;

public function __construct(KernelInterface $kernel)
{
parent::__construct($kernel);
$this->originalAutoExit = true;
}

/**
* Runs the current application.
*
* @param InputInterface $input An Input instance
* @param OutputInterface $output An Output instance
*
* @return integer 0 if everything went fine, or an error code
*
* @throws \Exception When doRun returns Exception
*
* @api
*/
public function run(InputInterface $input = null, OutputInterface $output = null)
{
//make parent method throw exceptions, so we can log it
$this->setCatchExceptions(false);

if (null === $input) {
$input = new ArgvInput();
}

if (null === $output) {
$output = new ConsoleOutput();
}

try {
$statusCode = parent::run($input, $output);
} catch (\Exception $e) {

/** @var $logger LoggerInterface */
$logger = $this->getKernel()->getContainer()->get('logger');

$message = sprintf(
'%s: %s (uncaught exception) at %s line %s while running console command `%s`',
get_class($e),
$e->getMessage(),
$e->getFile(),
$e->getLine(),
$this->getCommandName($input)
);
$logger->crit($message);

if ($output instanceof ConsoleOutputInterface) {
$this->renderException($e, $output->getErrorOutput());
} else {
$this->renderException($e, $output);
}
$statusCode = $e->getCode();

$statusCode = is_numeric($statusCode) && $statusCode ? $statusCode : 1;
}

if ($this->originalAutoExit) {
if ($statusCode > 255) {
$statusCode = 255;
}
// @codeCoverageIgnoreStart
exit($statusCode);
// @codeCoverageIgnoreEnd
}

return $statusCode;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we accomplish this same thing by not re-implementing so much of the parent run function? For example, couldn't we do $this->setCatchExceptions(false); and then call parent::run() inside a try-catch?

Also, I think doRun might be a more proper method to override, only because that's typically the purpose of the do* functions (to offer a clean override hook).

Thanks!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@weaverryan, thanks for reviewing!
Please look at https://github.com/symfony/symfony-docs/pull/1965/files#L1R128 - this is exactly what we do. I'd also prefer not to re-implement part of parent method, but unfortunately the way it is designed now, it either throws exceptions or renders them, not both. So we do setCatchExceptions(false), but then we have to take care of the rendering ourselves. I see how we can extract exception handling into a separate method in core, but even if we do, it will go into 2.2 only.

As for overriding doRun - I think that for this particular case it's better to override run() method, because we use setCatchExceptions(false) in our override and parent doRun can also change that. So, if we set our setting and then call parent::doRun() we may still have catchExceptions set to true. Actually, current doRun in the FrameworkBundle changes that for shell mode (Shell.php contains $this->application->setCatchExceptions(true);). While logging exceptions when working with shell doesn't make much sense, it's still a valid example of how settings can be modified in parent doRun, and we want to avoid it for this cookbook.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added an explanation on why we are re-implementing parent.

}

public function setAutoExit($bool)
{
// parent property is private, so we need to intercept it in setter
$this->originalAutoExit = (Boolean) $bool;
parent::setAutoExit($bool);
}

}

What happens above is we disable exception catching, so that parent run method
would throw the exceptions. When exception is caught, we simple log it by
accessing the ``logger`` service from the service container and then handle
the rest in the same way parent run method does that (Since parent :method:`run<Symfony\\Bundle\\FrameworkBundle\\Console\\Application::run>`
method will not handle exceptions rendering and status code handling when
`catchExceptions` is set to false, it has to be done in the overridden
method).

For our extended Application class to work properly with console shell mode
we have to do a small trick to intercept ``autoExit`` setter, and store the
setting in a different property, since the parent property is private.

Now to be able to use our extended ``Application`` class we need to adjust
``app/console`` script to use our class instead of the default::

// app/console

// ...
// replace the following line:
// use Symfony\Bundle\FrameworkBundle\Console\Application;
use Acme\DemoBundle\Console\Application;

// ...

That's it! Thanks to autoloader, our class will now be used instead of original
one.


Logging non-0 exit statuses
---------------------------

The logging capabilities of the console can be further extended by logging
non-0 exit statuses. This way you will know if a command had any errors, even
if no exceptions were thrown.

In order to do that, you'd have to modify ``run()`` method of your extended
`Application` class in the following way::

public function run(InputInterface $input = null, OutputInterface $output = null)
{
//make parent method throw exceptions, so we can log it
$this->setCatchExceptions(false);

// store autoExit value before resetting it - we'd need it later
$autoExit = $this->originalAutoExit;
$this->setAutoExit(false);

// ...

if ($autoExit) {
if ($statusCode > 255) {
$statusCode = 255;
}

// log non-0 exit codes along with command name
if ($statusCode !== 0) {
/** @var $logger LoggerInterface */
$logger = $this->getKernel()->getContainer()->get('logger');
$logger->warn(sprintf('Command `%s` exited with status code %d', $this->getCommandName($input), $statusCode));
}

// @codeCoverageIgnoreStart
exit($statusCode);
// @codeCoverageIgnoreEnd
}

return $statusCode;
}



1 change: 1 addition & 0 deletions cookbook/map.rst.inc
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
* :doc:`/cookbook/console/console_command`
* :doc:`/cookbook/console/usage`
* :doc:`/cookbook/console/generating_urls`
* :doc:`/cookbook/console/logging`

* :doc:`/cookbook/controller/index`

Expand Down