如何在命令行中开启日志

3.4 版本
维护中的版本

Console组件并不提供任何日志能力。一般来说,你要手动执行命令并观察输出,这就是为什么不提供日志了。然而,仍然会有一些场景你可能需要日志。例如,当你运行无人值守的命令行时,像是cron jobs或部署过的脚本,可能很自然地会使用Symfony自带的logging而不是配置其他工具来收集命令行的输出然后再处理它。如果你已经有一些“用于收集和分析Symfony日志”的现成的设置,(面对这种需求时)将会格外棘手。

基本上你需要两个日志场景:

  • 从你的命令中手动记录一些信息;
  • 记录未捕获的异常。

从一个命令中手动记录日志 

这十分简单。当你使用的是完整版框架并按照 "Console Commands" 一文所描述的来创建命令时,这个command继承的是 ContainerAwareCommand。这意味着你可以通过容器直接访问到标准的logger服务并用于做记录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// src/AppBundle/Command/GreetCommand.php
namespace AppBundle\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 Psr\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->warning('Yelled: '.$text);
        } else {
            $logger->info('Greeted: '.$text);
        }
 
        $output->writeln($text);
    }
}

根据你执行命令时所处的环境之不同 (也包括你的logging设置),你会在 var/logs/dev.logvar/logs/prod.log 文件中看到所记录的信息。

开启异常的自动日志 

要让控制台程序为所有命令自动地记录下未捕获异常,你可以使用 console events

首先在服务容器中,针对console exception事件配置一个监听:

1
2
3
4
5
6
7
# app/config/services.yml
services:
    app.listener.command_exception:
        class: AppBundle\EventListener\ConsoleExceptionListener
        arguments: ['@logger']
        tags:
            - { name: kernel.event_listener, event: console.exception }
1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- app/config/services.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<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="app.listener.command_exception" class="AppBundle\EventListener\ConsoleExceptionListener">
            <argument type="service" id="logger"/>
            <tag name="kernel.event_listener" event="console.exception" />
        </service>
    </services>
</container>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// app/config/services.php
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
 
$definitionConsoleExceptionListener = new Definition(
    'AppBundle\EventListener\ConsoleExceptionListener',
    array(new Reference('logger'))
);
$definitionConsoleExceptionListener->addTag(
    'kernel.event_listener',
    array('event' => 'console.exception')
);
$container->setDefinition(
    'app.listener.command_exception',
    $definitionConsoleExceptionListener
);

然后去实现真正的监听类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// src/AppBundle/EventListener/ConsoleExceptionListener.php
namespace AppBundle\EventListener;
 
use Symfony\Component\Console\Event\ConsoleExceptionEvent;
use Psr\Log\LoggerInterface;
 
class ConsoleExceptionListener
{
    private $logger;
 
    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }
 
    public function onConsoleException(ConsoleExceptionEvent $event)
    {
        $command = $event->getCommand();
        $exception = $event->getException();
 
        $message = sprintf(
            '%s: %s (uncaught exception) at %s line %s while running console command `%s`',
            get_class($exception),
            $exception->getMessage(),
            $exception->getFile(),
            $exception->getLine(),
            $command->getName()
        );
 
        $this->logger->error($message, array('exception' => $exception));
    }
}

在上述代码中,当任何一个命令抛出异常时,监听都会收到一个事件。通过从服务配置中传进来的logger服务,你可以把它直接记录下来。你的方法接收一个 ConsoleExceptionEvent 对象,里面有可以获取到事件和异常等相关信息的方法。

记录非0退出状态 

通过记录non-0 exit status,可以进一步扩展命令行的日志能力。当一个命令出现错误时,哪怕没有异常抛出,你就要用到此种方式。

先在服务容器中,为console terminate事件配置一个监听:

1
2
3
4
5
6
7
# app/config/services.yml
services:
    app.listener.command_error:
        class: AppBundle\EventListener\ErrorLoggerListener
        arguments: ['@logger']
        tags:
            - { name: kernel.event_listener, event: console.terminate }
1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- app/config/services.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<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="app.listener.command_error" class="AppBundle\EventListener\ErrorLoggerListener">
            <argument type="service" id="logger"/>
            <tag name="kernel.event_listener" event="console.terminate" />
        </service>
    </services>
</container>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// app/config/services.php
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
 
$definitionErrorLoggerListener = new Definition(
    'AppBundle\EventListener\ErrorLoggerListener',
    array(new Reference('logger'))
);
$definitionErrorLoggerListener->addTag(
    'kernel.event_listener',
    array('event' => 'console.terminate')
);
$container->setDefinition(
    'app.listener.command_error',
    $definitionErrorLoggerListener
);

然后去实现真正的监听类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// src/AppBundle/EventListener/ErrorLoggerListener.php
namespace AppBundle\EventListener;
 
use Symfony\Component\Console\Event\ConsoleTerminateEvent;
use Psr\Log\LoggerInterface;
 
class ErrorLoggerListener
{
    private $logger;
 
    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }
 
    public function onConsoleTerminate(ConsoleTerminateEvent $event)
    {
        $statusCode = $event->getExitCode();
        $command = $event->getCommand();
 
        if ($statusCode === 0) {
            return;
        }
 
        if ($statusCode > 255) {
            $statusCode = 255;
            $event->setExitCode($statusCode);
        }
 
        $this->logger->warning(sprintf(
            'Command `%s` exited with status code %d',
            $command->getName(),
            $statusCode
        ));
    }
}

本文,包括例程代码在内,采用的是 Creative Commons BY-SA 3.0 创作共用授权。

登录symfonychina 发表评论或留下问题(我们会尽量回复)