🛠 CombineLogger: нужен один логгер или несколько?

В прошлой статье о логах я рассказывал про PSR-3 и обещал показать свою реализацию CombineLogger. Выполняю обещание 🔥


🎯 Откуда взялся CombineLogger?

В проектах, над которыми я работаю, везде используется Psr\Log\LoggerInterface. Это стандарт, который позволяет не привязываться к конкретной библиотеке.

Но возникла проблема: интерфейс один, а логгеров нужно несколько.

Хочется писать в файл и отправлять в output или в GrayLog. А код везде принимает только один LoggerInterface.

Добавлять везде echo - не хорошо!

Решение: создать класс, который сам реализует Psr\Log\LoggerInterface, а внутри держит массив логгеров и передаёт вызов каждому.


📦 Код

<?php

namespace App\Infrastructure\Logger;

use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;

class CombineLogger implements LoggerInterface
{
    private array $loggers = [];
    
    //  Простые, но удобные методы :)

    public function addLogger(LoggerInterface $logger): void
    {
        $this->loggers[] = $logger;
    }

    public function log($level, $message, array $context = []): void
    {
        foreach ($this->loggers as $logger) {
            $logger->log($level, $message, $context);
        }
    }

    //  Реализуем все по LoggerInterface

    public function emergency($message, array $context = []): void
    {
        $this->log(LogLevel::EMERGENCY, $message, $context);
    }

    public function alert($message, array $context = []): void
    {
        $this->log(LogLevel::ALERT, $message, $context);
    }

    public function critical($message, array $context = []): void
    {
        $this->log(LogLevel::CRITICAL, $message, $context);
    }

    public function error($message, array $context = []): void
    {
        $this->log(LogLevel::ERROR, $message, $context);
    }

    public function warning($message, array $context = []): void
    {
        $this->log(LogLevel::WARNING, $message, $context);
    }

    public function notice($message, array $context = []): void
    {
        $this->log(LogLevel::NOTICE, $message, $context);
    }

    public function info($message, array $context = []): void
    {
        $this->log(LogLevel::INFO, $message, $context);
    }

    public function debug($message, array $context = []): void
    {
        $this->log(LogLevel::DEBUG, $message, $context);
    }
}

🎯 Как это использовать

Собираем логгеры в одном месте (например, в контейнере зависимостей):

use Monolog\Logger;
use Monolog\Handler\StreamHandler;

// Логгер в файл
$fileLogger = new Logger('app');
$fileLogger->pushHandler(new StreamHandler('/var/log/app.log'));

// Простой логгер для отладки (выводит в консоль)
$outputLogger = new Logger('output');
$outputLogger->pushHandler(new StreamHandler('php://output'));

$combineLogger = new CombineLogger();
$combineLogger->addLogger($fileLogger);
$combineLogger->addLogger($outputLogger);

// Везде в коде используем $combineLogger как обычный PSR-3 логгер
$combineLogger->error('Ошибка оплаты', ['order_id' => 123]);

👍 Что даёт этот подход:

  • Единый интерфейс: везде используется PSR-3, код не знает, сколько логгеров внутри
  • Гибкость: добавил новый канал = дописал addLogger() в одном месте
  • Простота: не нужно в каждом классе думать, куда логировать, не нужно править код вне инициализации логгера

🧠 Развитие идеи: стратегии

Простая цепочка вызовов - это хорошо, но можно пойти дальше.

Что если нужен не вызов всех логгеров, а другая логика? Например:

  • Резервный логгер: первый логгер упал - вызываем второй
  • Логгер по условию: в зависимости от уровня или контекста выбираем разные каналы
  • Асинхронный логгер: пишем в очередь, а не блокируем основной поток
interface LoggerStrategyInterface
{
    public function log(array $loggers, $level, $message, array $context): void;
}

class ChainStrategy implements LoggerStrategyInterface
{
    public function log(array $loggers, $level, $message, array $context): void
    {
        foreach ($loggers as $logger) {
            $logger->log($level, $message, $context);
        }
    }
}

class FallbackStrategy implements LoggerStrategyInterface
{
    public function log(array $loggers, $level, $message, array $context): void
    {
        foreach ($loggers as $logger) {
            try {
                $logger->log($level, $message, $context);
                return;   // Успешно — выходим
            } catch (\Throwable $e) {
                continue; // Пробуем следующий
            }
        }
    }
}

Тогда CombineLogger становится ещё гибче:

class CombineLogger implements LoggerInterface
{
    private array $loggers = [];
    private LoggerStrategyInterface $strategy;

    public function __construct(LoggerStrategyInterface $strategy)
    {
        $this->strategy = $strategy;
    }

    public function log($level, $message, array $context = []): void
    {
        $this->strategy->log($this->loggers, $level, $message, $context);
    }
    
    // остальные методы...
}

📌 PSR-3 + CombineLogger + Стратегии = код, который не боится любых требований к логированию.


💬 Обсудить пост:

🔥 И не забудь подписаться :)