本文最初是由Mauro Chojrin在Honeybadger Developer Blog上撰写的。
软件开发是一项复杂的努力。对于整个系统保持稳定且有用,有很多事情必须正当才能正确。在大多数情况下,对于在应用程序的生命周期中可能出现的所有可能情况,开发人员只能预见到很多。最重要的是,好的软件是一系列迭代的结果,每个迭代都基于从前使用的反馈而建立的。
此反馈有多种形式,最明显的是用户评论和投诉。但是,诸如“这不起作用!”之类的消息诸如此类。没有提供足够的信息来解决问题。我们作为开发人员的需求是有关向南进展时发生的事情的详细信息。
此信息通常可以在日志文件中找到;至少,这就是我们在做这种法医分析时所期望的。但是,为了使这些信息存在,我们必须非常积极地让我们的计划朝着目标迈进目标时留下线索。
考虑日志时需要回答两个问题:
- 您应该记录什么?
- 您应该在哪里存储日志?
第一个问题可能太具体了,无法提供一个一般答案,但是最好的规则是“记录任何您认为可能需要弄清楚出了什么问题的东西”。最终,这是有足够的信息和淹没服务器磁盘之间的微妙均衡。这不是一个容易的选择。
第二个问题更容易解决,因为只有很多地方可以将信息存储在计算环境中。
在本文中,我将分析三种方法来处理这项常见任务。前两个是内置功能,最后一个是一个受欢迎的第三方库:
trigger_error
error_log
- 独白
让我们挖掘,我们可以吗?
通过trigger_error记录错误
寻找日志记录机构时遇到的第一个工具是trigger_error function。
此功能非常易于使用;它采用一条消息来说明出了什么问题,并且一个整数号码向错误类型发出信号。可以使用pre-defined constants。
提供此数字其用法的一个示例如下:
trigger_error('Something went wrong', E_USER_ERROR);
这将产生类似于以下内容的输出:
PHP Fatal error: Something went wrong in php shell code on line 1
当然,如果您从命令行运行代码。如果要将其放在通过浏览器访问的PHP文件中,例如:
<?php
trigger_error('Something went wrong', E_USER_ERROR);
然后通过发行来提高内置网络服务器:
php -S localhost:8000
最终向http://localhost:8000
提出了请求,您会在控制台中看到与以下内容类似的内容:
[Wed Apr 20 15:15:21 2022] PHP 7.4.3 Development Server (http://localhost:8000) started
[Wed Apr 20 15:15:25 2022] 127.0.0.1:34614 Accepted
[Wed Apr 20 15:15:25 2022] 127.0.0.1:34616 Accepted
[Wed Apr 20 15:15:25 2022] PHP Fatal error: Something went wrong in /mnt/c/Users/mchoj/PhpstormProjects/Honeybadger/index.php on line 2
[Wed Apr 20 15:15:25 2022] 127.0.0.1:34614 [500]: GET / - Something went wrong in /mnt/c/Users/mchoj/PhpstormProjects/Honeybadger/index.php on line 2
[Wed Apr 20 15:15:25 2022] 127.0.0.1:34614 Closing
[Wed Apr 20 15:15:26 2022] 127.0.0.1:34620 Accepted
[Wed Apr 20 15:15:29 2022] 127.0.0.1:34620 Closed without sending a request; it was probably just an unused speculative preconnection
在浏览器中,您会看到以下内容:
此功能的一个特殊功能是它受解释器配置的影响很大。这意味着根据设置,例如error_reporting,display_errors和few others,该功能的实际行为可能会发生显着变化。
例如,如果要使用以下命令重新启动服务器:
php -S localhost:8000 -d display_errors=1
您会看到以下内容:
这在实践中的意思是,很难确保在不同的运行环境中产生一致的结果。这不是您真正想要的项目。
通过error_log记录错误
PHP提供的另一个内置工具是the error_log function。
此函数比trigger_error
更可预测,因为它仅取决于error_log设置。但是,其实际行为可能会根据其参数(尤其是message_type
)而发生巨大变化。
例如,如果将代码更改为:
<?php
error_log('Something went wrong');
并刷新页面,您将盯着空白屏幕;但是,在查看控制台输出时,您会发现以下内容:
[Wed Apr 20 15:39:41 2022] PHP 7.4.3 Development Server (http://localhost:8000) started
[Wed Apr 20 15:39:47 2022] 127.0.0.1:34648 Accepted
[Wed Apr 20 15:39:47 2022] 127.0.0.1:34646 Accepted
[Wed Apr 20 15:39:47 2022] Something went wrong
[Wed Apr 20 15:39:47 2022] 127.0.0.1:34646 [200]: GET /
[Wed Apr 20 15:39:47 2022] 127.0.0.1:34646 Closing
[Wed Apr 20 15:39:47 2022] 127.0.0.1:34650 Accepted
[Wed Apr 20 15:39:57 2022] 127.0.0.1:34648 Closed without sending a request; it was probably just an unused speculative preconnection
[Wed Apr 20 15:39:57 2022] 127.0.0.1:34648 Closing
[Wed Apr 20 15:39:57 2022] 127.0.0.1:34650 Closed without sending a request; it was probably just an unused speculative preconnection
[Wed Apr 20 15:39:57 2022] 127.0.0.1:34650 Closing
即使您使用相同的php -S localhost:8000 -d display_errors=1
来抬起服务器。
实际上,此函数将错误消息与WebServer默认情况下的错误消息一起存储。
您可以通过message_type
参数更改此行为。如果在调用函数时用数字3
填充它,您将要求解释器将消息输出到特定文件,该文件由下一个参数的值确定:destination
。这样:
<?php
error_log('Something went wrong', 3, __DIR__.'/log');
如果您刷新页面。您最终会出现相同的空白屏幕,但是服务器日志将有所不同:
[Wed Apr 20 15:46:11 2022] PHP 7.4.3 Development Server (http://localhost:8000) started
[Wed Apr 20 15:46:15 2022] 127.0.0.1:34652 Accepted
[Wed Apr 20 15:46:15 2022] 127.0.0.1:34654 Accepted
[Wed Apr 20 15:46:15 2022] 127.0.0.1:34652 [200]: GET /
[Wed Apr 20 15:46:15 2022] 127.0.0.1:34652 Closing
[Wed Apr 20 15:46:16 2022] 127.0.0.1:34658 Accepted
[Wed Apr 20 15:46:18 2022] 127.0.0.1:34658 Closed without sending a request; it was probably just an unused speculative preconnection
[Wed Apr 20 15:46:18 2022] 127.0.0.1:34658 Closing
[Wed Apr 20 15:46:18 2022] 127.0.0.1:34654 Closed without sending a request; it was probably just an unused speculative preconnection
[Wed Apr 20 15:46:18 2022] 127.0.0.1:34654 Closing
但是,正如预期的那样,如果您查看项目目录中存在的文件,您会发现一个新创建的log
One,如果您查看其内容,您会发现以下内容:
Something went wrong
error_log
和trigger_error
之间的主要区别之一是,对后者的呼叫很可能会导致执行中断,而前者只会简单地将消息写入适当的流并屈服后控制。
乍一看,您可能会得到这样的印象,即此功能几乎没有比其他功能旨在将字符串编写为文件的功能。
但是,如果您仔细观察,您会注意到error_log
的一个非常特殊的,当然有用的功能;它在内部处理锁定,这意味着您不必在高并发环境(例如Web服务器)中处理讨厌的比赛条件。
尽管trigger_error
或error_log
在技术上工作,但很明显,他们提供的开发人员体验充其量是最佳的。
一分钟考虑一下您在本地和远程使用这些工具中存储日志信息所需的内容,更不用说按严重性选择日志目的地。
当然,PHP社区可以使用更好的选择。继续阅读以发现专业工具。
通过独白记录的错误记录
到目前为止,我一直在讨论PHP本身所提供的,坦率地说,这不是很吸引人。不过,幸运的是,有很多开发人员参与了超越这些界限的推动,这使得出色的工具成为现实。 Monolog就是这种情况。
独白是一个面向对象的库,可以通过composer带入任何PHP项目,它提供了许多非常酷的功能,例如能够通过不同的频道发送相同消息的能力(文件,数据库,,和一封电子邮件)并在存储之前应用不同的过滤和格式化逻辑,使您可以构建真正复杂的记录机制,而不会过多麻烦。
让我们从一个简单的示例开始:您要保留访问您网站的每个IP地址的日志。
您的代码看起来如下:
<?php
$currentIp = $_SERVER['REMOTE_ADDR'];
error_log('Got a visitor from '.$currentIp, 3, __DIR__.'/ip.log');
下一步是通过使用以下命令来携带独白:
composer require monolog/monolog
将产生类似于以下输出的输出:
Loading composer repositories with package information
Updating dependencies
Lock file operations: 2 installs, 0 updates, 0 removals
- Locking monolog/monolog (2.5.0)
- Locking psr/log (3.0.0)
Writing lock file
Installing dependencies from lock file (including require-dev)
Package operations: 2 installs, 0 updates, 0 removals
- Installing psr/log (3.0.0): Extracting archive
- Installing monolog/monolog (2.5.0): Extracting archive
10 package suggestions were added by new dependencies, use `composer suggest` to see details.
Generating autoload files
1 package you are using is looking for funding.
Use the `composer fund` command to find out more!
PSR-4 autoloading configured. Use "namespace Mauro\Log;" in src/
Include the Composer autoloader with: require 'vendor/autoload.php';
安装完成后,您需要在代码中使用库。像往常一样,这一切始于包括自动加载定义:
<?php
require_once 'vendor/autoload.php';
$currentIp = $_SERVER['REMOTE_ADDR'];
error_log('Got a visitor from '.$currentIp, 3, __DIR__.'/ip.log');
现在,在我们继续进行重构之前,您需要了解一些概念。问题是,虽然独白登录到一个全新的水平,但这种力量带有价格标签...不,我不是在谈论金钱,而是在谈论学习曲线。
第一个是Logger
本身。该课程构成了整个图书馆的骨干。实际上,这是您大部分时间都会使用的。
您可以拥有所需的尽可能多的记录仪,每个记录仪都具有自己的配置。
每个记录器都通过其频道名称标识。例如,如果您将日志发送到同一文件,并且需要以后将它们进行区分。
这个想法是针对通过独白生成的日志,可以使用grep
等简单工具轻松解析。
记录器依靠其他三个类来实现其目的:
- 处理者
- 处理器
- formatters
独白处理程序
处理程序是最低级别的组件。处理程序的责任是回答“该日志应该存储在哪里?”的问题。独白库中定义了许多处理程序类。这是其中的一些:
- ErrorLogHandler:最简单的,只是围绕koude1的一些包装纸
- NativeMailerHandler:使用PHP的koude18函数将日志发送到电子邮件地址
- SQSHandler:将日志发送到AWS SQS队列。
独白处理器
处理器用于在传递给处理程序以进行存储之前将其转换 raw 日志数据。
通常,这意味着添加标准上下文信息,例如日期和时间,用户和IP。
图书馆与许多非常有用的处理器捆绑在一起,例如:
- WebProcessor添加了有关客户的信息,发出此消息的请求
- MemoryProcessor添加了有关内存使用的信息
- GitProcessor添加了有关涉及的git犯罪的信息
当然,您可以通过创建实现ProcessorInterface的新类来构建自己的处理器。
总而言之,处理器的目的是回答“应该记录什么 else ?”
的问题。独白格式
最后,格式化是要回答“如何存储消息?”
的问题。格式与处理程序结合使用,以添加围绕消息的格式信息,因此可以在特定环境中更好地渲染它们。
例如,如果您是通过电子邮件发送日志,则精心制作的HTML将使日志在电子邮件客户端中更容易读取。
行动中的独白
好吧,这是很多理论吗?让我们查看一些代码以理解我们一直在讨论的所有代码。
回到我们的小例子,我们需要的第一件事是Logger
:
<?php
require_once 'vendor/autoload.php';
use Monolog\Logger;
$logger = new Logger('my-app');
$currentIp = $_SERVER['REMOTE_ADDR'];
但是...日志将在哪里存储?我们需要一个Handler
!
让我们添加一个:
<?php
require_once 'vendor/autoload.php';
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
$logger = new Logger('my-app');
$handler = new StreamHandler(__DIR__.'/ip.log');
$currentIp = $_SERVER['REMOTE_ADDR'];
现在,我们有一个处理程序,将每个日志记录保存到与我们的应用程序同一目录中的文件ip.log
。
我们现在需要做的是将此处理程序附加到记录器上,例如:
<?php
require_once 'vendor/autoload.php';
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
$logger = new Logger('my-app');
$handler = new StreamHandler(__DIR__.'/ip.log');
$logger->pushHandler($handler);
$currentIp = $_SERVER['REMOTE_ADDR'];
现在,让我们使用honolog:
编写我们的第一个日志消息
<?php
require_once 'vendor/autoload.php';
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
$logger = new Logger('my-app');
$handler = new StreamHandler(__DIR__.'/ip.log');
$logger->pushHandler($handler);
$currentIp = $_SERVER['REMOTE_ADDR'];
$logger->info('Got a visitor from '.$currentIp);
info
方法只是可以称为消息严重性的众多方法之一。
如果您启动了本地Web服务器(php -S localhost:8000
)并访问http://localhost:8000
,则会在ip.log
文件中找到类似的内容:
[2022-04-29T11:41:39.121355+02:00] my-app.INFO: Got a visitor from 127.0.0.1 [] []
现在,假设您不仅要将此信息保存到本地文件,还要通过电子邮件发送给管理员。您可以使用koude26进行这样做:
<?php
require_once 'vendor/autoload.php';
use Monolog\Logger;
use Monolog\Handler\{StreamHandler, NativeMailerHandler};
$logger = new Logger('my-app');
$handler = new StreamHandler(__DIR__.'/ip.log');
$emailHandler = new NativeMailerHandler('you@yourdomain.com', 'Message from Honeybadger\'s App', 'app@localhost');
$logger->pushHandler($handler);
$logger->pushHandler($emailHandler);
$currentIp = $_SERVER['REMOTE_ADDR'];
$logger->info('Got a visitor from '.$currentIp);
如果您刷新页面,您将在文件中看到一个新条目,但收件箱中没有电子邮件。为什么发生这种情况?
NativeMailerHandler
旨在默认情况下仅对错误消息做出反应,因此您在这里有两个选项。您可以更改消息的严重性:
<?php
require_once 'vendor/autoload.php';
use Monolog\Logger;
use Monolog\Handler\{StreamHandler, NativeMailerHandler};
$logger = new Logger('my-app');
$handler = new StreamHandler(__DIR__.'/ip.log');
$emailHandler = new NativeMailerHandler('you@yourdomain.com', 'Message from HoneBadger\'s App', 'app@localhost');
$logger->pushHandler($handler);
$logger->pushHandler($emailHandler);
$currentIp = $_SERVER['REMOTE_ADDR'];
$logger->error('Got a visitor from '.$currentIp);
或者您可以让$emailHandler
考虑info
消息:
<?php
require_once 'vendor/autoload.php';
use Monolog\Logger;
use Monolog\Handler\{StreamHandler, NativeMailerHandler};
$logger = new Logger('my-app');
$handler = new StreamHandler(__DIR__.'/ip.log');
$emailHandler = new NativeMailerHandler('you@yourdomain.com', 'Message from Honeybadger\'s App', 'app@localhost', Logger::INFO);
$logger->pushHandler($handler);
$logger->pushHandler($emailHandler);
$currentIp = $_SERVER['REMOTE_ADDR'];
$logger->info('Got a visitor from '.$currentIp);
在这种情况下,您的收件箱应包含类似于以下信息的消息:
您可以看到,使用HTML格式器来制作此不错的电子邮件,而无需您做任何事情。您是否以使用独白的想法出售? :)
您可以在合适的情况下继续添加处理程序,格式化器和处理器;可能性是无限的。
与Honeybadger集成
现在,如果您熟悉Honeybadger,您已经知道它可以给您的开发团队带来的所有好处。
将其集成到单一动力的日志中不是很棒吗?好吧,猜怎么着?这真的很简单。
只需要一个Honeybadger\LogHandler作为独白处理程序的实例。
首先添加所需库:
composer require Honeybadger-io/Honeybadger-php
然后更新您的代码以使其看起来像:
<?php
require_once 'vendor/autoload.php';
use Monolog\Logger;
use Monolog\Handler\{StreamHandler, NativeMailerHandler};
use Honeybadger\{Honeybager, LogHandler};
$logger = new Logger('my-app');
$handler = new StreamHandler(__DIR__.'/ip.log');
$emailHandler = new NativeMailerHandler('you@yourdomain.com', 'Message from Honeybadger\'s App', 'app@localhost', Logger::INFO);
$honeybadger = Honeybadger\Honeybadger::new([
'api_key' => 'YOUR_API_KEY'
]);
$honeyBadgerHandler = new LogHandler($honeybadger);
$logger->pushHandler($handler);
$logger->pushHandler($emailHandler);
$logger->pushHandler($honeyBadgerHandler);
$currentIp = $_SERVER['REMOTE_ADDR'];
$logger->info('Got a visitor from '.$currentIp);
,在再次刷新页面后,您将在Honeybadger仪表板上找到这样的消息:
您的项目是基于Laravel的吗?你也掩盖了。阅读here以获取完整的集成指南。
结论
由于我们习惯了PHP,因此您可以在心跳中实现快速和差异的解决方案,并且有一些专业的解决方案需要更多的努力,但是从长远来看,它们的差异使它们变得非常值得麻烦。
您将为您的项目选择哪一个?