本文是非常有用的一篇文章,特意转载翻译
为什么我们需要日志系统
犯错是很常见的。
不仅是开发人员,在用户使用过程中也是如此。
如果在开发过程中我们完全可以控制了代码的运行过程,并且可以通过简单的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
ALERT
和 EMERGENCY
等级别通常通过附加通知(短信、电子邮件等)处理。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
,我们需要为 emergency
、alert
、critical
、error
、warning
、notice
、info
和 debug
方法编写实现,其中 对应于我们之前看过的等级。它们的实现归结为一个非常简单的原则——我们调用 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.log 和 errors.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)