登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  php教程

PHP 错误异常日志分层的 Monolog 配置

来源:17golang原创

时间:2026-10-28 11:26:35 196浏览 收藏

PHP 项目把所有消息写进一个文件,排障时通常会遇到两个相反的问题:业务信息太多,真正的错误被淹没;异常信息又太少,值班人员无法判断影响范围。Monolog 的分层关键不在于“多建几个日志文件”,而在于先定义级别,再把 Handler 按目的绑定到不同输出。

官方地址:https://seldaek.github.io/monolog/

要点速览
  • 业务轨迹写入普通日志,异常和错误单独进入告警候选日志。
  • Handler 的最低级别决定接收范围,bubble 决定记录是否继续向下传播。
  • 上下文只保留排障所需字段,密码、令牌、Cookie 和原始请求体必须在进入 Logger 前剔除。

先把日志当成资产,再划分错误边界

建议先把记录分成三档:info 记录订单号、任务名和关键状态;warning 记录可恢复的降级、重试和外部依赖变慢;error 记录请求失败、未捕获异常和需要人工介入的结果。不要把每个调试细节都提升为 error,否则错误文件会失去告警价值。

从安全角度看,日志有三条攻击路径:用户输入可能制造伪造换行,异常上下文可能泄露凭据,日志文件权限过宽可能让同机进程读取隐私。因此“分层”同时要回答谁能读、保存多久、哪些字段不允许落盘。

用两个 StreamHandler 完成基础分层

下面的配置让业务日志接收 info 及以上记录,错误日志只接收 error 及以上记录。把错误 Handler 先压入栈,便于它先得到记录;两个 Handler 都保留传播,达到错误既进错误文件、又保留在业务日志中的效果。

# 安装 Monolog 3;生产环境使用 composer.lock 固定依赖版本
composer require monolog/monolog
pushHandler($appHandler);
$logger->pushHandler($errorHandler);

// 业务轨迹使用 info,字段只放可检索且不含秘密的标识
$logger->info('payment request accepted', ['order_id' => 'ORD-20261028-01']);

try {
    // 这里替换成真实的支付调用;异常必须保留原异常对象便于追踪
    throw new RuntimeException('payment provider timeout');
} catch (Throwable $exception) {
    // 不把请求体、Authorization 或 Cookie 直接放入 context
    $logger->error('payment request failed', [
        'order_id' => 'ORD-20261028-01',
        'exception' => $exception,
    ]);
}

这里的级别是“最低接收级别”:app.log 会接收 info、warning 和 error,error.log 只接收 error 及更严重记录。若把错误 Handler 的 bubble 改为 false,被它处理的记录将不再传给栈中更低位置的 Handler,除非你明确需要这种截断行为,否则不建议这样配置。

PHP Monolog Logger checkout 按 Level::Info 和 Level::Error 分到 app.log 与 error.log 的静态说明图
图1:Monolog Handler 分层说明图,展示业务日志与错误日志的接收边界。

异常上下文要可排障,也要可审计

异常对象适合交给 Monolog 的 Formatter 统一展开,但请求参数要先做白名单或脱敏。最稳妥的做法是只传递请求 ID、业务 ID、依赖名称和耗时,不把整个 $_POST、请求头或响应原文塞进上下文。

 $requestId ?? 'unknown',
    'route' => $routeName ?? 'unknown',
    'dependency' => 'payment-provider',
];

try {
    $result = $client->charge($payload);
    $logger->info('payment completed', $context + ['result' => 'accepted']);
} catch (Throwable $exception) {
    // 用户可控的错误文本不要直接拼接到多行日志;保留异常类型与内部消息
    $logger->error('payment failed', $context + [
        'exception_class' => $exception::class,
        'exception' => $exception,
    ]);
    throw $exception;
}

如果日志要被采集到集中式平台,还应在 Formatter 或 Processor 层统一遮蔽字段,并限制日志目录的读取用户。过滤规则要覆盖 password、token、authorization、cookie 等不同大小写形式;不要只靠开发者记住“这次别打印”。

PHP Monolog 异常上下文经过 Formatter、Processor 和敏感字段过滤后写入受控日志目录的安全边界说明图
图2:异常上下文与日志安全边界说明图,展示可记录字段和应拦截字段的关系。

用 PHP 诊断配置和检查表收口

Monolog 负责应用层记录,PHP 自身的诊断日志仍受 error_reporting、log_errors 和 error_log 控制。生产环境应关闭页面直接显示错误,把诊断输出送到受控的错误入口;不要把 PHP 的底层错误和业务异常重复注册成两份。

检查项正确结果常见偏差
级别info 进业务日志,error 额外进错误日志所有消息都用 error
传播按需保留 bubble,明确是否要截断错误只进一个文件而无意丢失
字段只记录 ID、路由、依赖和异常类型直接记录请求体与 Authorization
运维目录权限、轮转、保留期都有负责人日志无限增长或 Web 用户可下载

最后做一次故障演练:分别写入一条 info、warning 和 error,确认文件分布、格式和权限;再触发一次异常,确认没有重复告警,也没有敏感字段。检查通过后,再把错误日志接到值班系统,而不是直接把所有业务日志推给告警通道。

常见问题

为什么 error.log 里没有 warning?

因为它的最低级别设置为 Level::Error。需要收集 warning 时,应调整该 Handler 的级别或增加专门的 Handler,不要把所有调用点改成 error。

bubble 设为 false 会发生什么?

当前 Handler 处理记录后,记录不会继续传给后面的 Handler。它适合明确的“只发一次”或“到此为止”策略,普通分层通常保留为 true。

PHP 错误日志和 Monolog 错误日志要合并吗?

不必强行合并。先确定 PHP 诊断入口,再让应用异常由 Monolog 负责;关键是避免同一异常同时由全局处理器和业务 catch 块重复记录。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>