日志

hyperf/logger 组件是基于 psr/logger 实现的,默认使用 monolog/monolog 作为驱动,在 hyperf-skeleton 项目内默认提供了一些日志配置,默认使用 Monolog\Handler\StreamHandler, 由于 Swoole 已经对 fopen, fwrite 等函数进行了协程化处理,所以只要不将 useLocking 参数设置为 true,就是协程安全的。

安装

  1. composer require hyperf/logger

配置

hyperf-skeleton 项目内默认提供了一些日志配置,默认情况下,日志的配置文件为 config/autoload/logger.php ,示例如下:

  1. <?php
  2. return [
  3. 'default' => [
  4. 'handler' => [
  5. 'class' => \Monolog\Handler\StreamHandler::class,
  6. 'constructor' => [
  7. 'stream' => BASE_PATH . '/runtime/logs/hyperf.log',
  8. 'level' => \Monolog\Logger::DEBUG,
  9. ],
  10. ],
  11. 'formatter' => [
  12. 'class' => \Monolog\Formatter\LineFormatter::class,
  13. 'constructor' => [
  14. 'format' => null,
  15. 'dateFormat' => null,
  16. 'allowInlineLineBreaks' => true,
  17. ]
  18. ],
  19. ],
  20. ];

使用

  1. <?php
  2. declare(strict_types=1);
  3. namespace App\Service;
  4. use Psr\Container\ContainerInterface;
  5. use Hyperf\Logger\LoggerFactory;
  6. class DemoService
  7. {
  8. protected LoggerInterface $logger;
  9. public function __construct(LoggerFactory $loggerFactory)
  10. {
  11. // 第一个参数对应日志的 name, 第二个参数对应 config/autoload/logger.php 内的 key
  12. $this->logger = $loggerFactory->get('log', 'default');
  13. }
  14. public function method()
  15. {
  16. // Do something.
  17. $this->logger->info("Your log message.");
  18. }
  19. }

关于 monolog 的基础知识

我们结合代码来看一些 monolog 中所涉及到的基础概念:

  1. use Monolog\Formatter\LineFormatter;
  2. use Monolog\Handler\FirePHPHandler;
  3. use Monolog\Handler\StreamHandler;
  4. use Monolog\Logger;
  5. // 创建一个 Channel,参数 log 即为 Channel 的名字
  6. $log = new Logger('log');
  7. // 创建两个 Handler,对应变量 $stream 和 $fire
  8. $stream = new StreamHandler('test.log', Logger::WARNING);
  9. $fire = new FirePHPHandler();
  10. // 定义时间格式为 "Y-m-d H:i:s"
  11. $dateFormat = "Y n j, g:i a";
  12. // 定义日志格式为 "[%datetime%] %channel%.%level_name%: %message% %context% %extra%\n"
  13. $output = "%datetime%||%channel||%level_name%||%message%||%context%||%extra%\n";
  14. // 根据 时间格式 和 日志格式,创建一个 Formatter
  15. $formatter = new LineFormatter($output, $dateFormat);
  16. // 将 Formatter 设置到 Handler 里面
  17. $stream->setFormatter($formatter);
  18. // 将 Handler 推入到 Channel 的 Handler 队列内
  19. $log->pushHandler($stream);
  20. $log->pushHandler($fire);
  21. // clone new log channel
  22. $log2 = $log->withName('log2');
  23. // add records to the log
  24. $log->warning('Foo');
  25. // add extra data to record
  26. // 1. log context
  27. $log->error('a new user', ['username' => 'daydaygo']);
  28. // 2. processor
  29. $log->pushProcessor(function ($record) {
  30. $record['extra']['dummy'] = 'hello';
  31. return $record;
  32. });
  33. $log->pushProcessor(new \Monolog\Processor\MemoryPeakUsageProcessor());
  34. $log->alert('czl');
  • 首先, 实例化一个 Logger, 取个名字, 名字对应的就是 channel
  • 可以为 Logger 绑定多个 Handler, Logger 打日志, 交由 Handler 来处理
  • Handler 可以指定需要处理哪些 日志级别 的日志, 比如 Logger::WARNING, 只处理日志级别 >=Logger::WARNING 的日志
  • 谁来格式化日志? Formatter, 设置好 Formatter 并绑定到相应的 Handler
  • 日志包含哪些部分: "%datetime%||%channel||%level_name%||%message%||%context%||%extra%\n"
  • 区分一下日志中添加的额外信息 contextextra: context 由用户打日志时额外指定, 更加灵活; extra 由绑定到 Logger 上的 Processor 固定添加, 比较适合收集一些 常见信息

更多用法

封装 Log

可能有些时候您更想保持大多数框架使用日志的习惯,那么您可以在 App 下创建一个 Log 类,并通过 __callStatic 魔术方法静态方法调用实现对 Logger 的取用以及各个等级的日志记录,我们通过代码来演示一下:

  1. namespace App;
  2. use Hyperf\Logger\LoggerFactory;
  3. use Hyperf\Utils\ApplicationContext;
  4. class Log
  5. {
  6. public static function get(string $name = 'app')
  7. {
  8. return ApplicationContext::getContainer()->get(LoggerFactory::class)->get($name);
  9. }
  10. }

默认使用 Channel 名为 app 来记录日志,您也可以通过使用 Log::get($name) 方法获得不同 ChannelLogger, 强大的 容器(Container) 帮您解决了这一切

stdout 日志

框架组件所输出的日志在默认情况下是由 Hyperf\Contract\StdoutLoggerInterface 接口的实现类 Hyperf\Framework\Logger\StdoutLogger 提供支持的,该实现类只是为了将相关的信息通过 print_r() 输出在 标准输出(stdout),即为启动 Hyperf终端(Terminal) 上,也就意味着其实并没有使用到 monolog 的,那么如果想要使用 monolog 来保持一致要怎么处理呢?

是的, 还是通过强大的 容器(Container).

  • 首先, 实现一个 StdoutLoggerFactory 类,关于 Factory 的用法可在 依赖注入 章节获得更多详细的说明。
  1. <?php
  2. declare(strict_types=1);
  3. namespace App;
  4. use Psr\Container\ContainerInterface;
  5. class StdoutLoggerFactory
  6. {
  7. public function __invoke(ContainerInterface $container)
  8. {
  9. return Log::get('sys');
  10. }
  11. }
  • 声明依赖, 使用 StdoutLoggerInterface 的地方, 由实际依赖的 StdoutLoggerFactory 实例化的类来完成
  1. // config/autoload/dependencies.php
  2. return [
  3. \Hyperf\Contract\StdoutLoggerInterface::class => \App\StdoutLoggerFactory::class,
  4. ];

不同环境下输出不同格式的日志

上面这么多的使用, 都还只在 monolog 中的 Logger 这里打转, 这里来看看 HandlerFormatter

  1. // config/autoload/logger.php
  2. $appEnv = env('APP_ENV', 'dev');
  3. if ($appEnv == 'dev') {
  4. $formatter = [
  5. 'class' => \Monolog\Formatter\LineFormatter::class,
  6. 'constructor' => [
  7. 'format' => "||%datetime%||%channel%||%level_name%||%message%||%context%||%extra%\n",
  8. 'allowInlineLineBreaks' => true,
  9. 'includeStacktraces' => true,
  10. ],
  11. ];
  12. } else {
  13. $formatter = [
  14. 'class' => \Monolog\Formatter\JsonFormatter::class,
  15. 'constructor' => [],
  16. ];
  17. }
  18. return [
  19. 'default' => [
  20. 'handler' => [
  21. 'class' => \Monolog\Handler\StreamHandler::class,
  22. 'constructor' => [
  23. 'stream' => 'php://stdout',
  24. 'level' => \Monolog\Logger::INFO,
  25. ],
  26. ],
  27. 'formatter' => $formatter,
  28. ],
  29. ];
  • 默认配置了名为 defaultHandler, 并包含了此 Handler 及其 Formatter 的信息
  • 获取 Logger 时, 如果没有指定 Handler, 底层会自动把 default 这一 Handler 绑定到 Logger
  • dev(开发)环境: 日志使用 php://stdout 输出到 标准输出(stdout), 并且 Formatter 中设置 allowInlineLineBreaks, 方便查看多行日志
  • 非 dev 环境: 日志使用 JsonFormatter, 会被格式为 json, 方便投递到第三方日志服务

日志文件按日期轮转

如果您希望日志文件可以按照日期轮转,可以通过 Mongolog 已经提供了的 Monolog\Handler\RotatingFileHandler 来实现,配置如下:

修改 config/autoload/logger.php 配置文件,将 Handler 改为 Monolog\Handler\RotatingFileHandler::class,并将 stream 字段改为 filename 即可。

  1. <?php
  2. return [
  3. 'default' => [
  4. 'handler' => [
  5. 'class' => Monolog\Handler\RotatingFileHandler::class,
  6. 'constructor' => [
  7. 'filename' => BASE_PATH . '/runtime/logs/hyperf.log',
  8. 'level' => Monolog\Logger::DEBUG,
  9. ],
  10. ],
  11. 'formatter' => [
  12. 'class' => Monolog\Formatter\LineFormatter::class,
  13. 'constructor' => [
  14. 'format' => null,
  15. 'dateFormat' => null,
  16. 'allowInlineLineBreaks' => true,
  17. ],
  18. ],
  19. ],
  20. ];

如果您希望再进行更细粒度的日志切割,也可通过继承 Monolog\Handler\RotatingFileHandler 类并重新实现 rotate() 方法实现。

配置多个 Handler

用户可以修改 handlers 让对应日志组支持多个 handler。比如以下配置,当用户投递一个 INFO 级别以上的日志时,会在 hyperf.loghyperf-debug.log 写入日志。 当用户投递一个 DEBUG 级别日志时,只会在 hyperf-debug.log 中写入日志。

  1. <?php
  2. declare(strict_types=1);
  3. use Monolog\Handler;
  4. use Monolog\Formatter;
  5. use Monolog\Logger;
  6. return [
  7. 'default' => [
  8. 'handlers' => [
  9. [
  10. 'class' => Handler\StreamHandler::class,
  11. 'constructor' => [
  12. 'stream' => BASE_PATH . '/runtime/logs/hyperf.log',
  13. 'level' => Logger::INFO,
  14. ],
  15. 'formatter' => [
  16. 'class' => Formatter\LineFormatter::class,
  17. 'constructor' => [
  18. 'format' => null,
  19. 'dateFormat' => null,
  20. 'allowInlineLineBreaks' => true,
  21. ],
  22. ],
  23. ],
  24. [
  25. 'class' => Handler\StreamHandler::class,
  26. 'constructor' => [
  27. 'stream' => BASE_PATH . '/runtime/logs/hyperf-debug.log',
  28. 'level' => Logger::DEBUG,
  29. ],
  30. 'formatter' => [
  31. 'class' => Formatter\JsonFormatter::class,
  32. 'constructor' => [
  33. 'batchMode' => Formatter\JsonFormatter::BATCH_MODE_JSON,
  34. 'appendNewline' => true,
  35. ],
  36. ],
  37. ],
  38. ],
  39. ],
  40. ];

结果如下

  1. ==> runtime/logs/hyperf.log <==
  2. [2019-11-08 11:11:35] hyperf.INFO: 5dc4dce791690 [] []
  3. ==> runtime/logs/hyperf-debug.log <==
  4. {"message":"5dc4dce791690","context":[],"level":200,"level_name":"INFO","channel":"hyperf","datetime":{"date":"2019-11-08 11:11:35.597153","timezone_type":3,"timezone":"Asia/Shanghai"},"extra":[]}
  5. {"message":"xxxx","context":[],"level":100,"level_name":"DEBUG","channel":"hyperf","datetime":{"date":"2019-11-08 11:11:35.597635","timezone_type":3,"timezone":"Asia/Shanghai"},"extra":[]}