Knotlog:面向 PHP 的广泛日志记录

发布: (2026年1月2日 GMT+8 21:35)
9 min read
原文: Dev.to

Source: Dev.to

正如 loggingsucks.com 所精彩阐述的,传统日志在现代应用中根本失效。问题是系统性的:

日志是为错误的时代而建。
它们起源于系统是单体且运行在单台服务器上的时代。如今,一个用户请求可能会涉及 15+ 个服务、多个数据库、缓存以及消息队列。传统的日志实践根本无法适应这种分布式现实。

字符串搜索本质上有缺陷。
当你在日志中搜索 "user-123" 时,会发现它在各服务中以数十种不同格式不一致地记录。日志被优化用于 写入,而不是 查询——这使得事后调查变成一次通过 grep 会话进行的痛苦考古探险。

海量数据,洞察甚少。
现代应用每秒会产生数万行日志。一次成功的请求可能会在代码库中散布 17 条不同的日志语句。乘以成千上万的并发用户,这就变成了难以管理的噪音,缺乏调试所需的上下文信息。

loggingsucks.com 提出的解决方案既简单又强大:宽事件(或规范日志行)。不要在代码中到处散布 log.info() 语句,而是为每个请求、每个服务发出一条完整、上下文丰富的结构化事件。该单一事件应包含所有可能有用的调试信息:用户上下文、业务数据、特性标记、错误细节等。

Enter Knotlog

Knotlog 是一个 PHP 库,实现了宽日志(wide logging)作为一等公民模式。Knotlog 不把宽事件视为事后考虑,而是将其设为默认的日志记录方式。

核心概念很直接:在整个请求生命周期中逐步构建上下文,然后在结束时发出单个结构化事件。将调试从考古式的 grep 会话转变为使用结构化、可查询数据的分析查询。

如何使用 Knotlog

安装

composer require knotlog/knotlog

Knotlog 需要 PHP 8.4 或更高版本。

基本用法

基本工作流程很简单:创建一个 Log 实例,在请求处理过程中累积上下文,最后将其写出:

use Knotlog\Log;
use Knotlog\Writer\FileWriter;

$log = new Log();

// 在整个请求过程中构建上下文
$log->set('user_id', $userId);
$log->set('request_method', $_SERVER['REQUEST_METHOD']);
$log->set('request_path', $_SERVER['REQUEST_URI']);
$log->set('subscription_tier', $user->subscriptionTier);
$log->set('cart_value', $cart->total());
$log->set('response_status', 200);
$log->set('duration_ms', $duration);

// 在请求结束时,发出宽事件
(new FileWriter())->write($log);

Log 类实现了 JsonSerializable,因此可以直接编码为 JSON,供结构化日志系统使用。

异常日志记录

Knotlog 提供了 ExceptionLog 类来捕获丰富的异常上下文,包括堆栈跟踪:

use Knotlog\Misc\ExceptionLog;

try {
    // 可能抛出异常的代码
} catch (Throwable $throwable) {
    $log->set('exception', ExceptionLog::fromThrowable($throwable));
}

Log 类还提供了一个便利的 hasError() 方法,如果 exceptionerror 键任意一个被设置,则返回 true——这在实现日志抽样时特别有用,可确保错误始终被捕获。

PSR‑15 中间件集成

Knotlog 最强大的功能之一是它与 PSR‑15 中间件的无缝集成。这使您能够在无需手动为每个端点添加仪器的情况下自动捕获请求和响应上下文。

LogRequestResponse

LogRequestResponse 中间件会自动记录请求和响应的元数据:

use Knotlog\Http\LogRequestResponse;

// Logs request and response metadata
$stack->add(new LogRequestResponse($log));

它在将请求传递给下一个处理程序之前捕获请求,然后在返回时捕获响应——自动将两者添加到您的 wide 事件中。

LogRequestError

LogRequestError 中间件捕获未捕获的异常,记录它们,并生成相应的错误响应:

use Knotlog\Http\LogRequestError;
use Knotlog\Http\ServerErrorResponseFactory;

$errorFactory = new ServerErrorResponseFactory($responseFactory, $streamFactory);

// Logs uncaught exceptions and outputs an error response
$stack->add(new LogRequestError($errorFactory, $log));

当异常在您的中间件堆栈中向上冒泡时,此中间件会将其捕获为 ExceptionLog,将其添加到您的 wide 事件中,并生成一个干净的错误响应。

LogResponseError

LogResponseError 中间件将任何状态码为 400 以上的响应标记为错误:

use Knotlog\Http\LogResponseError;

// Sets error to the reason phrase for 400+ status codes
$stack->add(new LogResponseError($log));

这在日志抽样时尤为有用——即使您只抽样少量成功请求,也能确保错误响应始终被记录。

Middleware Ordering

中间件应按以下顺序排列(从第一到最后):

  1. LogResponseError – 标记错误响应
  2. LogRequestResponse – 记录请求/响应元数据
  3. LogRequestError – 捕获未捕获的异常

注意: 某些框架对其中间件堆栈使用 后进先出(last‑in‑first‑out)顺序,请相应调整。

Source:

PSR‑3 日志集成

虽然 Knotlog 旨在取代传统的日志模式,但如果需要将结构化事件转发到现有的日志管道,也可以将其与 PSR‑3 日志器集成。(实现细节省略)

LoggerWriter – 与 PSR‑3 日志器的桥接

许多应用已经具备 PSR‑3 日志器基础设施。LoggerWriter 提供了一座桥梁,可通过任何兼容 PSR‑3 的日志器路由 wide events

use Knotlog\Writer\LoggerWriter;

// 与任意 PSR‑3 日志器(Monolog 等)一起使用
$writer = new LoggerWriter($psrLogger);

// 写入日志事件
$writer->write($log);

LoggerWriter 使用 PSR‑3 消息插值来格式化你的 wide events。它会自动将日志事件路由到相应的日志级别:

级别使用时机占位符
error()日志包含错误或异常{error}
info()所有其他日志事件{message}

完整的日志上下文会传递给日志器,允许其根据自身实现对结构化数据进行格式化和处理。

你可以自定义消息键和错误键,以匹配自己的日志模式:

$writer = new LoggerWriter($psrLogger, messageKey: 'msg', errorKey: 'err');

此集成让你在不放弃现有日志基础设施的前提下采用 wide‑logging 模式。你的 PSR‑3 日志器将收到丰富的结构化上下文,而不是零散的字符串消息。

附加功能

日志采样

SampledWriter 装饰器允许您对日志事件进行抽样,同时 始终 捕获错误:

use Knotlog\Writer\SampledWriter;
use Knotlog\Writer\FileWriter;

// Sample 1 in 100 requests (1%)
$writer = new SampledWriter(new FileWriter(), 100);

$writer->write($log);

所有包含错误或异常的日志事件都会被写入,不受抽样率的影响。抽样仅适用于成功请求,这对于高流量应用至关重要,因为对每个成功请求进行日志记录成本过高。

控制台集成

Knotlog 提供 Symfony Console 事件监听器,以对 CLI 命令进行广泛日志记录:

use Knotlog\Console\LogCommandError;
use Knotlog\Console\LogCommandEvent;
use Symfony\Component\Console\ConsoleEvents;

// Log command metadata on execution
$eventDispatcher->addListener(
    ConsoleEvents::COMMAND,
    new LogCommandEvent($log)
);

// Log command error context on failure
$eventDispatcher->addListener(
    ConsoleEvents::ERROR,
    new LogCommandError($log)
);

关键要点

传统日志记录问 “我的代码在做什么?”
宽日志记录问 “这个请求发生了什么?”

这种视角的转变——结合 Knotlog 的一流 PSR‑15 中间件和 PSR‑3 集成——将日志记录从调试的事后想法转变为强大的可观测性工具。

  • 停止在代码中到处散布日志语句。
  • 开始使用 Knotlog 捕获全面、可查询的上下文。

了解更多请访问 github.com/shadowhand/knotlog,并在 loggingsucks.com 阅读其理念。

Back to Blog

相关文章

阅读更多 »