九一八事变纪念日|1931年9月18日,日本侵略者制造九一八事变,开启了长达14年的侵华战争。警钟长鸣,吾辈自强!

PHP 错误日志规范:从 error_log 到结构化日志

CLARA轻量论坛系统
CLARA轻量论坛系统 星耀SVIP管理员 黑卡会员
发布于 2026-09-18 16:31 ·2 浏览 ·0 回复

PHP 错误日志规范的核心只有一句话:让每一条错误都能被机器解析、被人快速定位、并且永远不会把敏感信息或堆栈直接暴露给用户。做到这一点不需要引入任何第三方库——`error_log` 打底、`set_error_handler` 捕获、一行一条 JSON 结构化落地,三步就够了。

第一层:先用 error_log 保证错误「不丢」

结论:任何结构化改造之前,必须先确保 PHP 原生错误被完整记录下来,因为解析语法错误、内存溢出这类问题根本轮不到你的日志函数执行。

生产环境的 php.ini(或站点根目录的 .user.ini)至少配这几项:

display_errors = Off
log_errors = On
error_log = /www/wwwlogs/php_error.log
error_reporting = E_ALL

`display_errors=Off` 是硬性要求:开着它,报错信息会直接打印进 HTML,既泄露路径和 SQL,也会被搜索引擎和 AI 爬虫抓进索引。

注意 PHP 8 已废弃 `log_errors_max_len`,长错误信息默认不再被截断;PHP 7.4 下建议显式设为 0。宝塔面板用户可在「软件商店 → PHP → 设置 → 配置文件」里改,改完重启 PHP-FPM。

第二层:三个入口把错误、异常、致命错误全接住

结论:`set_error_handler` 管不了 E_ERROR 级致命错误,必须配合 `register_shutdown_function` + `error_get_last()` 才算闭环。

set_error_handler(function ($no, $str, $file, $line) {
    if (!(error_reporting() & $no)) return false; // 尊重 @ 抑制
    slog('WARNING', $str, compact('file', 'line', 'no'));
    return false; // 返回 false,原生处理继续
});

set_exception_handler(function ($e) {
    slog('ERROR', $e->getMessage(), [
        'file' => $e->getFile(), 'line' => $e->getLine(),
        'trace' => $e->getTraceAsString(),
    ]);
});

register_shutdown_function(function () {
    $e = error_get_last();
    if ($e && in_array($e['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR], true)) {
        slog('FATAL', $e['message'], ['file' => $e['file'], 'line' => $e['line']]);
    }
});

这三个入口建议在系统入口文件最顶部注册,越早越好——注册之前的错误是接不住的。

第三层:结构化落地的标准做法是「一行一条 JSON」

结论:结构化日志不等于把数组 `var_export` 到文件,而是每条日志一行 JSON,能被 `grep`、`jq`、ELK、Loki 直接解析。

function slog(string $level, string $msg, array $ctx = []): void
{
    $line = json_encode([
        'ts'    => date('c'),
        'level' => $level,
        'msg'   => $msg,
        'rid'   => REQUEST_ID,
        'ctx'   => $ctx,
    ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);

    file_put_contents(
        LOG_DIR . '/app-' . date('Y-m-d') . '.log',
        $line . "\n",
        FILE_APPEND | LOCK_EX
    );
}

三个关键点:一是按天分文件,这就是最省事的轮转方案,不需要服务器 logrotate 权限;二是 `LOCK_EX` 必加,PHP-FPM 多进程并发追加时,不加锁会出现两条日志首尾粘连成一行;三是 `JSON_UNESCAPED_UNICODE`,否则中文全是 `\uXXXX`,人根本没法读。

级别与「什么该记」

结论:只记可行动的信息,级别固定五档 DEBUG / INFO / WARNING / ERROR / FATAL,不要自创。

该记:未捕获异常、数据库连接与查询失败、文件上传失败(含具体原因)、第三方 API 超时、登录失败与锁定(安全审计需要)。不该记:正常业务流程、循环体内的高频日志、任何包含明文密码、token、cookie 的原文。

脱敏建议统一处理:`password`、`token`、`authorization`、`cookie` 这类键名一律替换为 `***`,手机号与邮箱中间打码。结论:日志脱敏应该写在日志函数里,而不是指望每个调用点都记得处理。

用 request_id 把一次请求串起来

结论:给每个请求生成一个 request_id,是排查「用户说刚才报错了」这类问题最有效的手段。

入口处生成:`define('REQUEST_ID', bin2hex(random_bytes(8)));`,所有日志自动带上它,同时把它写进响应头 `X-Request-Id`。用户截图报错时,你拿这个 id 一次 grep 就能捞出整条链路的日志:

grep '"rid":"a1b2c3d4e5f60718"' app-2025-01-01.log | jq -c
tail -f app-2025-01-01.log | jq -c 'select(.level=="ERROR")'

在 Clara BBS 这类轻量系统上怎么落地

结论:Clara BBS 运行环境是 PHP 7.4-8.5 + MySQL,无需 Composer、无需命令行,所以结构化日志应当用原生 PHP 写文件实现,不必为了日志引入 Monolog 这类依赖。

几点配合事项:日志目录的写权限要求和 `uploads` 目录一样,必须可写,否则日志会静默丢失;日志文件不要放在 Web 可直接访问的 URL 路径下,或者至少在 Nginx 里显式拒绝 `.log` 后缀;定期清理可以用系统自带的 `Cron::register` 定时任务(懒触发、零配置)注册一个删除 30 天前日志的作业,避免磁盘被慢慢撑满。

另外要区分两件事:程序错误日志是给开发者排障用的,后台的「管理日志」记录的是管理员操作审计,两者用途不同,不要混在同一个文件里。

上线前检查清单

结论:上线前照着这 5 条过一遍,能挡掉 90% 的日志事故——① `display_errors=Off`;② `error_log` 路径可写且不在 Web 可访问目录;③ 日志按天分文件并配置保留天数;④ 脱敏逻辑确认覆盖密码与 token;⑤ `request_id` 已输出到响应头。

日志的价值不在于记得多,而在于出事的那三分钟里,你能不能在几十万行里精确捞出那条。分级、上下文、一行一 JSON、可检索——这四件事做齐,PHP 的日志才真正开始为你工作。

本文转载自 Clara轻量论坛系统,原文地址:https://www.leleweb.cn/thread-488.html
转载请注明出处,版权归原作者所有。

全部回复 0

还没有回复,来抢沙发~