应按用途划分独立日志通道(如'security'、'request'),避免混写于单文件,以解决过滤难、轮转策略冲突和行为不一致问题;使用monolog封装可配置logger实例,构造函数延迟初始化,校验级别与上下文,异常时降级处理。

log_record 这种简单函数封装在小项目里够用,但一旦业务变复杂、日志要分通道、要结构化、要异步写入,就会迅速失控——不是逻辑散落在各处,就是改一个级别就得全局搜替换。
用 Monolog 封装成可配置的 Logger 实例
Monolog 是事实标准,它把「日志行为」拆成三块:Logger(入口)、Handler(怎么写)、Formatter(写成啥样),你只管组合,不用重写底层。
- 不要直接 new
Monolog\Logger后硬编码 handler,而是封装一个工厂方法或依赖注入容器配置 - 每个 channel(如
'security'、'request')应对应独立的Logger实例,避免混写 - 生产环境必须禁用
StreamHandler写到php://stdout,改用文件路径并确保目录可写
示例片段(非完整类,仅说明结构):
$logger = new Monolog\Logger('request');
$handler = new Monolog\Handler\StreamHandler('/var/log/app/request.log', Monolog\Level::Info);
$handler->setFormatter(new Monolog\Formatter\JsonFormatter());
$logger->pushHandler($handler);
为什么不能把所有日志塞进一个文件
多个模块共用一个 app.log 看似省事,实际会带来三个问题:
- 查问题时无法快速过滤:比如安全审计只要看
security日志,结果得从几万行里 grep - 权限与轮转策略冲突:错误日志要保留 90 天,访问日志只需 7 天,合并在一个文件就无法单独配置
- Handler 行为不一致:
security日志可能需要同步写 + 邮件告警,而debug日志只需异步缓冲,混用会互相拖慢
建议按用途划分 channel,并在配置中明确每个 channel 的:
- 目标路径或远程 endpoint
- 最低记录级别(
Monolog\Level::Warning) - Formatter 类型(JSON / Line)
- 是否启用缓冲(
BufferHandler)
自定义封装类时,__construct 和 log 方法最容易踩坑
自己写 Logger 类不是不行,但以下几点极易翻车:
- 在
__construct里直接创建 handler 并写日志,导致构造失败时整个对象不可用,且无法 mock 测试 - 把
log方法设计成接受$level, $message, $context三个参数,却不校验$level是否合法(比如传了'warn'而不是Monolog\Level::Warning) - 上下文
$context未做深度克隆或 json_encode 安全检查,遇到Closure或资源句柄直接报错 - 忘记设置
error_log的maxlen参数,超长消息被截断,关键字段丢失
正确做法是:
- 构造函数只接收配置数组,延迟初始化 handler
- 使用
Monolog\Level常量作为 level 参数,而非字符串 - 对
$context先做json_encode(..., JSON_THROW_ON_ERROR)预检 - 所有写操作包裹 try/catch,失败时 fallback 到
error_log或静默丢弃(视级别而定)
真正难的不是写日志,而是让日志在出问题时依然能写出来——比如磁盘满、权限错、NFS 挂掉。这些边界情况,Monolog 的 FingersCrossedHandler 或自研的降级策略,比手写 file_put_contents 可靠得多。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











