PHP如何生成有效的日志log文件

2023年2月20日服务端开发评论46,497字数 7723阅读25分44秒阅读模式

本文是非常有用的一篇文章,特意转载翻译

为什么我们需要日志系统

犯错是很常见的。

不仅是开发人员,在用户使用过程中也是如此。

如果在开发过程中我们完全可以控制了代码的运行过程,并且可以通过简单的debugging看到错误的行为,那么在生产环境中,同样的情况就不会那么容易调查了。

在这种情况下,对我们有帮助的是错误日志。但是为了获得其使用的最大价值,我们应该正确地使用它。

Logging——维护此类日志的过程,它有助于检测隐藏的错误、了解用户问题并简单地整理出真正发生的事情。在最简单的实现中,日志被写入文本文件并包含事件的确切时间和描述。有很多方法可以做到这一点,幸运的事——最佳实践很久以前就定义好了。

在本文中,我们将整理出如何在 PHP 应用程序中组织日志记录、如何有效地使用它以及哪些库可能有用。

PSR-3 标准. 日志级别

PSR 是针对 PHP 开发人员的一般建议的集合。它包含代码风格的提示、一些接口和其他建议。其中一份文档 (PSR-3) 专门用于日志文件的实现。

我建议您开始熟悉这些建议及其提供的日志记录级别。

<?php

namespace Psr\Log;

class LogLevel
{
    const EMERGENCY = 'emergency';
    const ALERT     = 'alert';
    const CRITICAL  = 'critical';
    const ERROR     = 'error';
    const WARNING   = 'warning';
    const NOTICE    = 'notice';
    const INFO      = 'info';
    const DEBUG     = 'debug';
}

PSR-3 定义了 8 种不同的消息级别。正确使用它们将简化错误查找并提高故障反应速度。让我们更深入地了解何时使用它们。

  • DEBUG——详细揭示事件细节的调试信息;
  • INFO——任何有趣的事件。例如:用户已登录;
  • NOTICE——预期行为范围内的重要事件;
  • WARNING——仍然不是错误的例外情况。例如使用过时的方法或错误的 API 请求;
  • ERROR – 要监视的错误,但不需要紧急修复;
  • CRITICAL——临界状态或事件。例如:组件不可用或未处理的异常;
  • ALERT – 错误和需要在最短时间内解决的事件。比如数据库不可用;
  • EMERGENCY - 整个应用程序/系统完全失灵。

使用这些级别意味着只需将其作为某种前缀添加到您的日志消息中。例如:

[2021-01-01 12:10:35] website.INFO: User has changed his password

ALERTEMERGENCY 等级别通常通过附加通知(短信、电子邮件等)处理。INFO 允许您重现整个用户的操作序列,而 DEBUG 将简化精确值和函数结果的获取。

PSR-3. Logger Interface

除了具有级别的类之外,PSR-3 还为我们提供了一个用于实现我们自己的Logger的接口 - LoggerInterface。它的实现非常有用,因为大多数现有库都支持它,如果您决定用另一个替换Logger,只需连接一个新类即可。

LoggerInterface 需要根据之前审查的级别实施日志记录方法。让我们创建自己的Logger类,它将对应于该接口并将消息写入文本文件。

但首先,让我们用 Composer 下载 PSR-3 代码。

composer req psr/log

我们下载的包中包含几个类、特征和接口,其中包括我们之前回顾过的 LogLevel 和我们目前感兴趣的 LoggerInterface。让我们创建一个实现该接口的新类。

重要提示:确保包含自动加载器类 (vendor/autoload.php)。

<?php
// index.php

// Composer's autoloader
require_once 'vendor/autoload.php';
// Our new Logger class
require_once 'src/FileLogger.php';
<?php
// src/FileLogger.php

// If `use` was not added automatically, 
// be sure to add it yourself
use Psr\Log\LoggerInterface;

// Implement the downloaded interface
class FileLogger implements LoggerInterface
{
    // ...
}

我们创建了类,但是为了让它满足标准的要求,需要把接口中描述的所有方法都写出来。其中最重要的是 log。登录文件的主要逻辑都会写在里面。

class FileLogger implements LoggerInterface
{
    // ...
    public function log($level, $message, array $context = []): void
    {
        // Current date in 1970-12-01 23:59:59 format
        $dateFormatted = (new \DateTime())->format('Y-m-d H:i:s');

        // Build the message with the current date, log level, 
        // and the string from the arguments
        $message = sprintf(
            '[%s] %s: %s%s',
            $dateFormatted,
            $level,
            $message,
            PHP_EOL // Line break
        );

        // Writing to the file `devto.log`
        file_put_contents('devto.log', $message, FILE_APPEND);
        // FILE_APPEND flag prevents flushing the file content on each call 
        // and simply adds a new string to it
    }
    // ...
}

为了完全满足 LoggerInterface,我们需要为 emergencyalertcriticalerrorwarningnoticeinfodebug 方法编写实现,其中 对应于我们之前看过的等级。它们的实现归结为一个非常简单的原则——我们调用 log 方法,将必要的级别传递给它。

class FileLogger implements LoggerInterface
{
    // ...
    public function emergency($message, array $context = []): void
    {
        // Use the level from LogLevel class
        $this->log(LogLevel::EMERGENCY, $message, $context);
    }

    public function alert($message, array $context = []): void
    {
        $this->log(LogLevel::ALERT, $message, $context);
    }
    // and so on...
    // ...
}

Use of logger

现在我们的类实现了 PSR-3 标准提出的接口,我们可以很容易地在任何地方使用它。例如,在文件 index.php 中:

<?php
// index.php

// Composer's autoloader
require_once 'vendor/autoload.php';
// Our new Logger class
require_once 'src/FileLogger.php';

$logger = new FileLogger();
$logger->debug('Message from index.php');

或者在任何其他类中:

<?php

use Psr\Log\LoggerInterface;

class ExampleService
{
    /** @var LoggerInterface */
    private $logger;

    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }

    public function doSomeAction(): void
    {
        // do some work
        $this->logger->debug('Message from ExampleService');
    }
}

请注意,我们没有将最终实现(FileLogger)指定为构造函数参数的类型,而是指定了 PSR-3 标准的接口。这非常方便,因为它使我们能够非常轻松地将使用过的Logger替换为支持此接口的任何其他Logger。

Context

您可能已经注意到 LoggerInterface 的所有方法都包含 $context 参数。为什么需要它?

$context 参数旨在传输辅助信息,通常是动态信息。例如,如果您正在制作调试条目(调试级别),则可以将变量的值传递给 $context 参数。

为了应用这个参数,我们需要在日志方法中支持它。让我们改进它,假设 $context 是一个数组。

class FileLogger implements LoggerInterface
{
    // ...
    public function log($level, $message, array $context = []): void
    {
        $dateFormatted = (new \DateTime())->format('Y-m-d H:i:s');

        // Let's convert $context array into json
        $contextString = json_encode($context);
        $message = sprintf(
            '[%s] %s: %s %s%s',
            $dateFormatted,
            $level,
            $message,
            $contextString, // Add context string
            PHP_EOL
        );

        file_put_contents('devto.log', $message, FILE_APPEND);
    }
    // ...
}

现在,在Logger调用的任何地方,我们都可以使用第二个参数传递一组附加信息。

<?php
// index.php
//...

$userName = 'Elijah';
$userEmail = 'elijah@dev.to';

$logger = new FileLogger();
$logger->debug('Message from index.php', [
    'user_name' => $userName,
    'user_email' => $userEmail,
]);

结果,我们将得到如下形式的记录:

[2021-09-02 13:00:24] debug: Message from index.php {"user_name":"Elijah","user_email":"elijah@dev.to"}

关于 $context 参数,有一个非常简单的规则:

任何动态信息都应该在 $context 参数中传递,而不是在消息中传递。

也就是说,如果您使用 sprintf 或字符串变量的串联将消息形成日志,则很可能可以将此信息放入 $context 参数中。

遵循此规则可简化日志中的搜索,因为它消除了预测(或计算)变量值的需要。

Monolog 组件库

尽管日志记录原理很简单,但在这方面有很大的修改空间。我们可以支持其他记录格式,实现 SMS 发送,或者只是允许您更改最终日志文件的名称。

幸运的是,所有这些已经在大多数库中实现了很长时间,其中最常见的一种是 monolog

该软件包最显着的优点包括:

  • 全面支持 PSR-3;
  • 支持不同的日志处理原则,取决于级别;
  • 支持通道名称(Logger名称);
  • 非常广泛的框架支持。

要开始使用这个优秀的工具,我们将使用 Composer 安装它。

Monolog 的使用

monolog 库的主要思想是处理程序。

它们允许您为记录事件设置特定行为。

例如,要将消息写入文本文件,我们将使用“StreamHandler”。让我们用加载的库替换我们类的使用。

<?php

// index.php

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

// Composer's autoloader
require_once 'vendor/autoload.php';

// We pass a channel name as the argument for Logger
$logger = new Logger('devto-demo');
// Connect the handler. Argument for StreamHandler is a path to a file
$logger->pushHandler(new StreamHandler('devto.log'));

// Then we leave everything as it was
$userName = 'Elijah';
$userEmail = 'elijah@dev.to';
$logger->debug('Message from index.php', [
    'user_name' => $userName,
    'user_email' => $userEmail,
]);

如果我们运行这段代码,以下条目将出现在 devto.log 文件中:

[2021-09-02T13:16:14.122686+00:00] devto-demo.DEBUG: Message from index.php {"user_name":"Elijah","user_email":"elijah@dev.to"} []

这与我们之前的非常相似,只是添加了应用的名称 (devto-demo)。

Monolog 处理程序的一个重要特性是它们可以设置它们工作的级别。例如,我们可以将所有的错误写在一个单独的文件中。

<?php

// index.php

use Monolog\Handler\StreamHandler;
use Monolog\Logger;
use Psr\Log\LogLevel;

// ...
$logger = new Logger('devto-demo');
$logger->pushHandler(new StreamHandler('devto.log'));
$logger->pushHandler(new StreamHandler('errors.log', LogLevel::ERROR));

// ...
$logger->emergency('It is not even an error. It is EMERGENCY!!!');

连接到 ERROR 级别的处理程序将获取 ERROR 级别及更高级别的整体记录。因此,对 emergency 方法的调用落在两个文件中:devto.logerrors.log

这种将记录简单划分为级别的做法极大地简化了我们对错误的反应。毕竟,我们不再需要在日志的所有其他条目中寻找它们。这是一个非常简单和有用的功能。

Log records of the request

在我们项目开发的过程中,日志的阅读非常简单。它们一致且清晰。但是,当多个人使用该产品时,日志可能会混在一起,而且混淆的程度远远超过它们的帮助。

有一个非常简单的技巧可以解决这个问题。使用唯一的会话 ID,而不是应用的名称。您可以使用内置函数 session_id() 获取它。(会话必须使用 session_start() 启动)

让我们看一下这种技术的实现示例:

<?php
// index.php

// Start the session
session_start();

// ...

// Pass the session id as a channel name
$logger = new Logger(session_id());
// ...

这样一个简单的重构给我们带来了什么?

一个非常重要的功能是根据用户请求对所有记录进行分组。

// First request records
[2021-09-02T13:35:54.155043+00:00] b30m8k1fvmf638au7ph0edb3o5.DEBUG: Message from index.php {"user_name":"Elijah","user_email":"elijah@dev.to"} []
[2021-09-02T13:35:54.156800+00:00] b30m8k1fvmf638au7ph0edb3o5.EMERGENCY: It is not even an error. It is EMERGENCY!!! [] []
// Another request records. They have different ids
[2021-09-02T13:36:03.528474+00:00] u7fi04mn99h0timg148rles1um.DEBUG: Message from index.php {"user_name":"Elijah","user_email":"elijah@dev.to"} []
[2021-09-02T13:36:03.529421+00:00] u7fi04mn99h0timg148rles1um.EMERGENCY: It is not even an error. It is EMERGENCY!!! [] []

更多内容

  • Monolog 支持很多非常有用的现成的处理程序,值得关注:

    • TelegramBotHandler – 通过 Telegram Bot 发送日志。对于高级别的日志记录非常有用;
    • SlackHandler – 与上一个非常相似,但将记录发送到 Slack;
    • SwiftMailerHandler – 允许通过电子邮件发送日志;
    • ChromePHPHandler – 在实时模式下直接从 Chrome 浏览器访问日志!

结论

日志记录是一个简单而重要的工具。它将帮助您在早期阶段修复错误,确保新版本代码中没有任何问题,调查您的用户案例并全面了解项目。

最主要的是记住简单的规则:

  • 遵循 PSR-3 将使您更容易替换代码中的Logger类,并允许您使用外部库;
  • 不同的日志记录级别将帮助您专注于重要的事情;
  • 将动态信息分离到$context中将简化通过日志的搜索;
  • Monolog 库实现了几乎所有可能的程序员需求。一定要花时间研究它;
  • 使用session ID,您可以为每个请求分离日志条目;
  • 写很多额外的日志总比不添加一个重要的日志要好。

原文链接:Investigating an incident: how to log effectively (PHP)

匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定