日志

虽然 CakePHP 核心 Configure 类设置可以帮助你查看幕后发生了什么,但有时你需要将数据记录到磁盘以找出问题所在。 对于像 SOAP、AJAX 和 REST API 这样的技术,调试可能会很困难。

日志还可以用于了解应用程序随时间的运行情况。 使用了什么搜索词? 我的用户看到了哪些类型的错误? 特定查询执行了多少次?

在 CakePHP 中,日志记录是通过 log() 函数完成的。 它由 LogTrait 提供,这是许多 CakePHP 类的共同祖先。 如果上下文是一个 CakePHP 类(控制器、组件、视图……),你可以记录你的数据。 你也可以直接使用 Log::write()。 请参阅 写入日志

日志配置

日志 Log 的配置应该在应用程序的引导阶段完成。 config/app.php 文件就是为此而设计的。 你可以根据应用程序需要定义任意多个日志记录器。 日志记录器应该使用 Cake\Log\Log 进行配置。 一个例子是

use Cake\Log\Engine\FileLog;
use Cake\Log\Log;

// Classname using logger 'class' constant
Log::setConfig('info', [
    'className' => FileLog::class,
    'path' => LOGS,
    'levels' => ['info'],
    'file' => 'info',
]);

// Short classname
Log::setConfig('debug', [
    'className' => 'File',
    'path' => LOGS,
    'levels' => ['notice', 'debug'],
    'file' => 'debug',
]);

// Fully namespaced name.
Log::setConfig('error', [
    'className' => 'Cake\Log\Engine\FileLog',
    'path' => LOGS,
    'levels' => ['warning', 'error', 'critical', 'alert', 'emergency'],
    'file' => 'error',
]);

以上代码创建了三个日志记录器,分别名为 infodebugerror。 每个记录器都被配置为处理不同级别的消息。 它们还将日志消息存储在不同的文件中,以便我们可以将调试/通知/信息日志与更严重的错误日志分开。 请参阅 使用级别 部分,了解有关不同级别及其含义的更多信息。

创建配置后,你无法更改它。 相反,你应该使用 Cake\Log\Log::drop()Cake\Log\Log::setConfig() 丢弃配置并重新创建配置。

还可以通过提供闭包来创建日志记录器。 这在需要完全控制日志记录器对象构建方式时非常有用。 闭包必须返回构建的日志记录器实例。 例如

Log::setConfig('special', function () {
    return new \Cake\Log\Engine\FileLog(['path' => LOGS, 'file' => 'log']);
});

配置选项也可以作为 DSN 字符串提供。 这在使用环境变量或 PaaS 提供商时非常有用

Log::setConfig('error', [
    'url' => 'file:///full/path/to/logs/?levels[]=warning&levels[]=error&file=error',
]);

警告

如果你没有配置日志引擎,日志消息将不会被存储。

错误和异常日志

错误和异常也可以被记录。 通过在 config/app.php 文件中配置相应的值。 当调试为 true 时,错误将被显示,当调试为 false 时,错误将被记录。 要记录未捕获的异常,将 log 选项设置为 true。 请参阅 配置 以了解更多信息。

写入日志

写入日志文件可以通过两种不同的方式完成。 第一种是使用静态 Cake\Log\Log::write() 方法

Log::write('debug', 'Something did not work');

第二种是使用在使用 LogTrait 的任何类上可用的 log() 快捷函数。 调用 log() 会在内部调用 Log::write()

// Executing this inside a class using LogTrait
$this->log('Something did not work!', 'debug');

每次调用 Cake\Log\Log::write() 时,所有已配置的日志流都会按顺序写入。 如果你没有配置任何日志引擎,log() 将返回 false 并且不会写入任何日志消息。

在消息中使用占位符

如果你需要记录动态定义的数据,可以在日志消息中使用占位符,并在 $context 参数中提供键值对数组

// Will log `Could not process for userid=1`
Log::write('error', 'Could not process for userid={user}', ['user' => $user->id]);

没有定义键的占位符将不会被替换。 如果你需要使用带括号的文字单词,必须转义占位符

// Will log `No {replace}`
Log::write('error', 'No \\{replace}', ['replace' => 'no']);

如果你在日志占位符中包含对象,这些对象必须实现以下方法之一

  • __toString()

  • toArray()

  • __debugInfo()

使用级别

CakePHP 支持标准的 POSIX 日志级别集。 每个级别代表越来越高的严重程度

  • 紧急:系统不可用

  • 警报:必须立即采取行动

  • 严重:严重状况

  • 错误:错误状况

  • 警告:警告状况

  • 通知:正常但重要的状况

  • 信息:信息消息

  • 调试:调试级消息

在配置日志记录器和编写日志消息时,你可以通过名称引用这些级别。 或者,你可以使用像 Cake\Log\Log::error() 这样的便捷方法,以明确指示日志级别。 使用不在上述级别中的级别会导致异常。

注意

当日志记录器配置中的 levels 设置为空值时,它将接收任何级别的消息。

日志范围

通常,你希望为应用程序的不同子系统或部分配置不同的日志记录行为。 以电子商务商店为例。 你可能希望以不同于其他不太重要的日志的方式处理订单和支付日志。

CakePHP 将此概念公开为日志范围。 写入日志消息时,可以包含一个范围名称。 如果为此范围配置了日志记录器,日志消息将被定向到这些日志记录器。 例如

use Cake\Log\Engine\FileLog;

// Configure logs/shops.log to receive all levels, but only
// those with `orders` and `payments` scope.
Log::setConfig('shops', [
    'className' => FileLog::class,
    'path' => LOGS,
    'levels' => [],
    'scopes' => ['orders', 'payments'],
    'file' => 'shops.log',
]);

// Configure logs/payments.log to receive all levels, but only
// those with `payments` scope.
Log::setConfig('payments', [
    'className' => FileLog::class,
    'path' => LOGS,
    'levels' => [],
    'scopes' => ['payments'],
    'file' => 'payments.log',
]);

Log::warning('this gets written only to shops.log', ['scope' => ['orders']]);
Log::warning('this gets written to both shops.log and payments.log', ['scope' => ['payments']]);

范围也可以作为单个字符串或数字索引数组传递。 请注意,使用此形式将限制传递更多数据作为上下文的能力

Log::warning('This is a warning', ['orders']);
Log::warning('This is a warning', 'payments');

注意

当日志记录器配置中的 scopes 设置为空数组或 null 时,它将接收任何范围的消息。 将其设置为 false 将只匹配没有范围的消息。

写入文件日志

顾名思义,FileLog 将日志消息写入文件。写入的日志消息级别决定了存储消息的文件名。如果没有提供级别,则使用 LOG_ERR,它会写入错误日志。默认的日志位置是 **logs/$level.log**

// Executing this inside a CakePHP class
$this->log("Something didn't work!");

// Results in this being appended to logs/error.log
// 2007-11-02 10:22:02 Error: Something didn't work!

为了使日志记录正常工作,配置的目录必须可由 Web 服务器用户写入。

在配置记录器时,您可以配置其他/备用 FileLog 位置。FileLog 接受一个 path,允许使用自定义路径

Log::setConfig('custom_path', [
    'className' => 'File',
    'path' => '/path/to/custom/place/'
]);

FileLog 引擎接受以下选项

  • size 用于实现基本的日志文件轮转。如果日志文件大小达到指定的大小,则通过将时间戳附加到文件名来重命名现有文件,并创建新的日志文件。可以是整数字节值或人类可读的字符串值,例如 ‘10MB’、‘100KB’ 等。默认值为 10MB。

  • rotate 在删除日志文件之前,将日志文件轮转指定的次数。如果值为 0,则删除旧版本而不是轮转它们。默认值为 10。

  • mask 设置为创建文件的权限。如果留空,则使用默认权限。

注意

将自动创建缺少的目录,以避免使用 FileEngine 时出现不必要的错误。

日志记录到 Syslog

在生产环境中,强烈建议您设置系统以使用 syslog 而不是文件记录器。这将执行得更好,因为任何写入都将以(几乎)非阻塞方式完成,并且您可以单独配置操作系统记录器以轮转文件、预处理写入或对您的日志使用完全不同的存储。

使用 syslog 与使用默认 FileLog 引擎几乎相同,您只需要将 Syslog 指定为用于日志记录的引擎即可。以下配置片段将用 syslog 替换默认的记录器,这应该在 **config/bootstrap.php** 文件中完成

Log::setConfig('default', [
    'engine' => 'Syslog'
]);

Syslog 日志记录引擎接受的配置数组理解以下键

  • format: 一个 sprintf 模板字符串,包含两个占位符,第一个用于错误级别,第二个用于消息本身。此键有助于在记录的消息中添加有关服务器或进程的附加信息。例如:%s - Web Server 1 - %s 在替换占位符后将类似于 error - Web Server 1 - An error occurred in this request。此选项已弃用。您应该使用 日志记录格式化程序 代替。

  • prefix: 将作为前缀添加到每个记录消息的字符串。

  • flag: 用于打开与记录器连接的整数标志,默认情况下将使用 LOG_ODELAY。有关更多选项,请参见 openlog 文档

  • facility: 在 syslog 中使用的日志插槽。默认情况下使用 LOG_USER。有关更多选项,请参见 syslog 文档

创建日志引擎

日志引擎可以是您的应用程序的一部分,也可以是插件的一部分。例如,如果您有一个名为 DatabaseLog 的数据库记录器。作为您的应用程序的一部分,它将放置在 **src/Log/Engine/DatabaseLog.php** 中。作为插件的一部分,它将放置在 **plugins/LoggingPack/src/Log/Engine/DatabaseLog.php** 中。要配置日志引擎,您应该使用 Cake\Log\Log::setConfig()。例如,配置我们的 DatabaseLog 将如下所示

// For src/Log
Log::setConfig('otherFile', [
    'className' => 'Database',
    'model' => 'LogEntry',
    // ...
]);

// For plugin called LoggingPack
Log::setConfig('otherFile', [
    'className' => 'LoggingPack.Database',
    'model' => 'LogEntry',
    // ...
]);

配置日志引擎时,使用 className 参数来定位和加载日志处理程序。所有其他配置属性都作为数组传递给日志引擎的构造函数。

namespace App\Log\Engine;
use Cake\Log\Engine\BaseLog;

class DatabaseLog extends BaseLog
{
    public function __construct(array $config = [])
    {
        parent::__construct($config);
        // ...
    }

    public function log($level, string $message, array $context = [])
    {
        // Write to the database.
    }
}

CakePHP 要求所有日志引擎都实现 Psr\Log\LoggerInterface。类 CakeLogEngineBaseLog 是一种满足接口的简单方法,因为它只需要您实现 log() 方法。

日志记录格式化程序

日志记录格式化程序允许您控制如何独立于存储引擎格式化日志消息。每个核心提供的日志记录引擎都附带一个格式化程序,该格式化程序配置为维护向后兼容的输出。但是,您可以调整格式化程序以适合您的需求。格式化程序与日志记录引擎一起配置

use Cake\Log\Engine\SyslogLog;
use App\Log\Formatter\CustomFormatter;

// Simple formatting configuration with no options.
Log::setConfig('error', [
    'className' => SyslogLog::class,
    'formatter' => CustomFormatter::class,
]);

// Configure a formatter with additional options.
Log::setConfig('error', [
    'className' => SyslogLog::class,
    'formatter' => [
        'className' => CustomFormatter::class,
        'key' => 'value',
    ],
]);

要实现您自己的日志记录格式化程序,您需要扩展 Cake\Log\Format\AbstractFormatter 或其子类之一。您需要实现的主要方法是 format($level, $message, $context),它负责格式化日志消息。

测试日志

要测试日志记录,请将 Cake\TestSuite\LogTestTrait 添加到您的测试用例中。 LogTestTrait 使用 PHPUnit 钩子来附加拦截应用程序正在发出的日志消息的日志引擎。捕获日志后,您可以对应用程序正在发出的日志消息执行断言。例如

namespace App\Test\TestCase\Controller;

use Cake\TestSuite\LogTestTrait;
use Cake\TestSuite\TestCase;

class UsersControllerTest extends TestCase
{
    use LogTestTrait;

    public function setUp(): void
    {
        parent::setUp();
        $this->setupLog([
            'error' => ['scopes' => ['app.security']]
        ]);
    }

    public function testResetPassword()
    {
        $this->post('/users/resetpassword', ['email' => '[email protected]']);
        $this->assertLogMessageContains('info', '[email protected] reset password', 'app.security');
    }
}

您使用 setupLog() 来定义您希望捕获并对其执行断言的日志消息。在发出日志后,您可以对日志内容或日志的缺失进行断言

  • assertLogMessage(string $level, string $expectedMessage, ?string $scope = null, string $failMsg = '') 断言找到日志消息。

  • assertLogMessageContains(string $level, string $expectedMessage, ?string $scope = null, string $failMsg = '') 断言日志消息包含子字符串。

  • assertLogAbsent(string $level, ?string $failMsg = '') 断言未捕获给定级别的日志消息。

LogTestTrait 将自动清理配置的任何记录器。

日志 API

class Cake\Log\Log

一个用于写入日志的简单类。

static Cake\Log\Log::setConfig($key, $config)
参数:
  • $name (string) – 要连接的记录器的名称,用于稍后删除记录器。

  • $config (array) – 记录器的配置信息和构造函数参数数组。

获取或设置记录器的配置。有关更多信息,请参见 日志记录配置

static Cake\Log\Log::configured
返回值:

已配置记录器的数组。

获取已配置记录器的名称。

static Cake\Log\Log::drop($name)
参数:
  • $name (string) – 您不再希望接收消息的记录器的名称。

static Cake\Log\Log::write($level, $message, $scope = [])

将消息写入所有配置的记录器。 $level 指示正在创建的日志消息的级别。 $message 是正在写入的日志条目的消息。 $scope 是正在创建日志消息的范围。

static Cake\Log\Log::levels

在没有参数的情况下调用此方法,例如: Log::levels() 以获取当前级别配置。

便捷方法

添加了以下便捷方法以使用适当的日志级别记录 $message

static Cake\Log\Log::emergency($message, $scope = [])
static Cake\Log\Log::alert($message, $scope = [])
static Cake\Log\Log::critical($message, $scope = [])
static Cake\Log\Log::error($message, $scope = [])
static Cake\Log\Log::warning($message, $scope = [])
static Cake\Log\Log::notice($message, $scope = [])
static Cake\Log\Log::info($message, $scope = [])
static Cake\Log\Log::debug($message, $scope = [])

日志记录特性

trait Cake\Log\LogTrait

一个提供日志记录快捷方法的特性

Cake\Log\LogTrait::log($msg, $level = LOG_ERR)

将消息记录到日志中。默认情况下,消息将作为错误消息记录。

使用 Monolog

Monolog 是一个流行的 PHP 日志记录器。由于它实现了与 CakePHP 日志记录器相同的接口,因此您可以在应用程序中将它们用作默认日志记录器。

使用 composer 安装 Monolog 后,使用 Log::setConfig() 方法配置日志记录器

// config/bootstrap.php

use Monolog\Logger;
use Monolog\Handler\StreamHandler;

Log::setConfig('default', function () {
    $log = new Logger('app');
    $log->pushHandler(new StreamHandler('path/to/your/combined.log'));

    return $log;
});

// Optionally stop using the now redundant default loggers
Log::drop('debug');
Log::drop('error');

如果您想为您的控制台配置不同的日志记录器,请使用类似的方法

// config/bootstrap_cli.php

use Monolog\Logger;
use Monolog\Handler\StreamHandler;

Log::setConfig('default', function () {
    $log = new Logger('cli');
    $log->pushHandler(new StreamHandler('path/to/your/combined-cli.log'));

    return $log;
});

// Optionally stop using the now redundant default CLI loggers
Configure::delete('Log.debug');
Configure::delete('Log.error');

注意

使用控制台特定的日志记录器时,请确保有条件地配置您的应用程序日志记录器。这将防止重复的日志条目。