
monolog 日志重复输出的根本原因是每次调用日志方法时都向同一个 logger 实例重复添加 handler,导致每条日志被多次写入。正确做法是为每次日志操作创建独立的 logger 实例,或复用 handler 而非反复 push。
monolog 日志重复输出的根本原因是每次调用日志方法时都向同一个 logger 实例重复添加 handler,导致每条日志被多次写入。正确做法是为每次日志操作创建独立的 logger 实例,或复用 handler 而非反复 push。
在使用 Monolog 构建自定义日志封装类时,一个常见但容易被忽视的问题是:日志消息被重复写入多次(如一条 info() 调用生成 2–3 行相同内容)。其根本原因在于 Logger::pushHandler() 的误用——该方法会将 Handler 追加到当前 Logger 的 Handler 栈中,而不会自动去重或清理历史 Handler。
回顾原始代码,$this->logger 是类级别单例对象,在 __construct() 中初始化一次,但每个 info()、debug() 等方法都会执行:
$this->logger->pushHandler($this->handler(Level::Info)); // ❌ 每次都新增一个 Handler $this->logger->info($log);
这意味着:
- 第 1 次调用
->info()→ 添加 1 个INFOHandler → 写入 1 行 - 第 2 次调用
->warning()→ 再添加 1 个WARNINGHandler,此时 Logger 共有 2 个 Handler(INFO + WARNING)→warning()同时触发两个 Handler,写入 2 行 - 第 3 次调用
->error()→ 又新增 1 个ERRORHandler,共 3 个 → 所有后续日志均被写入 3 次
这就是为何输出中 WARNING 出现了 3 次,且行数随调用次数线性增长。
✅ 正确解法是:避免复用同一 Logger 实例并反复 push Handler。推荐采用「按需创建轻量 Logger」模式——每次日志调用都新建一个 Logger,并仅绑定当前所需级别的 Handler。这既保证了隔离性,又无需手动管理 Handler 生命周期。
以下是优化后的专业实现(已适配 Monolog v3+,兼容 PHP 8.0+):
<?php namespace Plugin\Core;
use Monolog\Handler\StreamHandler;
use Monolog\Level;
use Monolog\Logger;
use Monolog\Formatter\LineFormatter;
class Log
{
private string $path;
private string $name;
public function __construct(string $name = 'wpmatrimony', ?string $path = null)
{
$this->name = $name;
$this->path = $path ?: WPMATRIMONY_PLUGIN_DIR . './logs/' . $name . '.log';
}
private function createLogger(Level $level): Logger
{
$logger = new Logger($this->name);
$logger->setTimezone(new \DateTimeZone('Asia/Kolkata')); // ✅ 修正时区名称大小写(ASIA/KOLKATA → Asia/Kolkata)
$handler = new StreamHandler($this->path, $level);
$formatter = new LineFormatter(
null, // format (default)
'd-m-Y H:i:s', // datetime format
false, // allowInlineLineBreaks
true // ignoreContext
);
$handler->setFormatter($formatter);
$logger->pushHandler($handler);
return $logger;
}
public function debug(string $message, array $context = []): void
{
$this->createLogger(Level::Debug)->debug($message, $context);
}
public function info(string $message, array $context = []): void
{
$this->createLogger(Level::Info)->info($message, $context);
}
public function warning(string $message, array $context = []): void
{
$this->createLogger(Level::Warning)->warning($message, $context);
}
public function error(string $message, array $context = []): void
{
$this->createLogger(Level::Error)->error($message, $context);
}
// 其他方法(notice/critical/alert/emergency)同理实现,略
}
? 关键改进说明:
- ✅ 无状态设计:
Log类自身不持有Logger实例,彻底规避 Handler 积累问题; - ✅ 精准级别控制:每个
createLogger()仅注册对应日志级别的 Handler,确保日志严格按等级写入; - ✅ 时区标准化:修正
ASIA/KOLKATA为标准 IANA 时区名Asia/Kolkata(Monolog 内部依赖DateTimeZone,非法名称可能导致静默失败); - ✅ 上下文支持:为所有方法增加
$context参数(如追踪请求 ID、用户 ID),便于结构化日志分析; - ✅ 类型声明强化:使用
string、Level等严格类型提示,提升 IDE 支持与运行时可靠性。
⚠️ 注意事项:
- 若需高性能高频日志(如每秒百次以上),可考虑缓存
StreamHandler实例(因文件句柄复用更高效),但必须确保 Handler 的level与日志调用一致; - 不建议在构造函数中预设全局 Handler —— 这会破坏日志级别的语义隔离;
- 生产环境应配合日志轮转(如
RotatingFileHandler)防止单文件过大。
通过此方案,以下调用将稳定输出预期结果:
$log = new Log('test-monolog');
$log->debug('debug'); // → 1 行 DEBUG
$log->info('info'); // → 1 行 INFO
$log->warning('warning'); // → 1 行 WARNING
每条日志严格、唯一、可追溯,真正实现“所写即所得”的日志可靠性。










