Как настроить универсальное логирование с использованием монолога?


Я пишу консольное приложение с компонентами Symfony2, и я хочу добавить отдельные каналы регистрации для моих служб, моих команд и так далее. Проблема: для создания нового канала требуется создать новый экземпляр Monolog, и я действительно не знаю, как справиться с этим универсальным способом, и без необходимости передавать обработчик потока, канал и соответствующий код для связывания одного и другого внутри всех служб.

Я сделал трюк, используя debug_backtrace():

public function log($level, $message, array $context = array ())
{
    $trace = array_slice(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3), 1);
    $caller = $trace[0]['class'] !== __CLASS__ ? $trace[0]['class'] : $trace[1]['class'];
    if (!array_key_exists($caller, $this->loggers))
    {
        $monolog = new Monolog($caller);
        $monolog->pushHandler($this->stream);
        $this->loggers[$caller] = $monolog;
    }
    $this->loggers[$caller]->log($level, $message, $context);
}
Независимо от того, откуда я вызываю свой регистратор, он создает канал для каждого класса, который его вызвал. Выглядит круто, но как только лесоруба называют тоннами времени, это убивает производительность.

Итак, вот мой вопрос:

Знаете ли вы лучший универсальный способ создать один отдельный канал монолога для каждого класса, который имеет свойство logger?


Приведенный выше код упакован для тестирование:

композитор.json

{
    "require" : {
        "monolog/monolog": "~1.11.0"
    }
}

тест.php

<?php

require('vendor/autoload.php');

use MonologLogger;
use MonologHandlerStreamHandler;

class Test
{

    public function __construct($logger)
    {
        $logger->info("test!");
    }

}

class Hello
{

    public function __construct($logger)
    {
        $logger->log(MonologLogger::ALERT, "hello!");
    }

}

class LeveragedLogger implements PsrLogLoggerInterface
{

    protected $loggers;
    protected $stream;

    public function __construct($file, $logLevel)
    {
        $this->loggers = array ();
        $this->stream = new StreamHandler($file, $logLevel);
    }

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

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

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

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

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

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

    public function log($level, $message, array $context = array ())
    {
        $trace = array_slice(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3), 1);
        $caller = $trace[0]['class'] !== __CLASS__ ? $trace[0]['class'] : $trace[1]['class'];
        if (!array_key_exists($caller, $this->loggers))
        {
            $monolog = new Logger($caller);
            $monolog->pushHandler($this->stream);
            $this->loggers[$caller] = $monolog;
        }
        $this->loggers[$caller]->log($level, $message, $context);
    }

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

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

}

$logger = new LeveragedLogger('php://stdout', Logger::DEBUG);

new Test($logger);
new Hello($logger);

использование

ninsuo:test3 alain$ php test.php
[2014-10-21 08:59:04] Test.INFO: test! [] []
[2014-10-21 08:59:04] Hello.ALERT: hello! [] []
3 2

3 ответа:

Что бы вы подумали о принятии решения, какой регистратор следует использовать непосредственно перед созданием потребителей? Это можно было легко сделать с помощью какого-нибудь DIC или, возможно, фабрики.

<?php

require('vendor/autoload.php');

use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Psr\Log\LoggerInterface;
use Monolog\Handler\HandlerInterface;

class Test
{
    public function __construct(LoggerInterface $logger)
    {
        $logger->info("test!");
    }
}

class Hello
{
    public function __construct(LoggerInterface $logger)
    {
        $logger->log(Monolog\Logger::ALERT, "hello!");
    }
}

class LeveragedLoggerFactory
{
    protected $loggers;
    protected $stream;

    public function __construct(HandlerInterface $streamHandler)
    {
        $this->loggers = array();
        $this->stream = $streamHandler;
    }

    public function factory($caller)
    {
        if (!array_key_exists($caller, $this->loggers)) {
            $logger = new Logger($caller);
            $logger->pushHandler($this->stream);
            $this->loggers[$caller] = $logger;
        }

        return $this->loggers[$caller];
    }
}

$loggerFactory = new LeveragedLoggerFactory(new StreamHandler('php://stdout', Logger::DEBUG));

new Test($loggerFactory->factory(Test::class));
new Hello($loggerFactory->factory(Hello::class));

Я, наконец, создал класс MonologContainer, который расширяет стандартный контейнер Symfony2 и вводит Logger в LoggerAware сервисы. Перегружая метод get() контейнера службы, я могу получить идентификатор службы и использовать его в качестве канала для регистратора.

<?php

namespace Fuz\Framework\Core;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Monolog\Handler\HandlerInterface;
use Monolog\Logger;
use Psr\Log\LoggerAwareInterface;

class MonologContainer extends ContainerBuilder
{

    protected $loggers = array ();
    protected $handlers = array ();
    protected $processors = array ();

    public function __construct(ParameterBagInterface $parameterBag = null)
    {
        parent::__construct($parameterBag);
    }

    public function pushHandler(HandlerInterface $handler)
    {
        foreach (array_keys($this->loggers) as $key)
        {
            $this->loggers[$key]->pushHandler($handler);
        }
        array_unshift($this->handlers, $handler);
        return $this;
    }

    public function popHandler()
    {
        if (count($this->handlers) > 0)
        {
            foreach (array_keys($this->loggers) as $key)
            {
                $this->loggers[$key]->popHandler();
            }
            array_shift($this->handlers);
        }
        return $this;
    }

    public function pushProcessor($callback)
    {
        foreach (array_keys($this->loggers) as $key)
        {
            $this->loggers[$key]->pushProcessor($callback);
        }
        array_unshift($this->processors, $callback);
        return $this;
    }

    public function popProcessor()
    {
        if (count($this->processors) > 0)
        {
            foreach (array_keys($this->loggers) as $key)
            {
                $this->loggers[$key]->popProcessor();
            }
            array_shift($this->processors);
        }
        return $this;
    }

    public function getHandlers()
    {
        return $this->handlers;
    }

    public function get($id, $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE)
    {
        $service = parent::get($id, $invalidBehavior);
        return $this->setLogger($id, $service);
    }

    public function setLogger($id, $service)
    {
        if ($service instanceof LoggerAwareInterface)
        {
            if (!array_key_exists($id, $this->loggers))
            {
                $this->loggers[$id] = new Logger($id, $this->handlers, $this->processors);
            }
            $service->setLogger($this->loggers[$id]);
        }
        return $service;
    }

}

Пример использования:

тест.php

#!/usr/bin/env php
<?php

use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Fuz\Framework\Core\MonologContainer;

if (!include __DIR__ . '/vendor/autoload.php')
{
    die('You must set up the project dependencies.');
}

$container = new MonologContainer();

$loader = new YamlFileLoader($container, new FileLocator(__DIR__));
$loader->load('services.yml');

$handler = new StreamHandler(__DIR__ ."/test.log", Logger::WARNING);
$container->pushHandler($handler);

$container->get('my.service')->hello();

услуги.yml

parameters:
    my.service.class: Fuz\Runner\MyService

services:

    my.service:
        class: %my.service.class%

Моя служба.php

<?php

namespace Fuz\Runner;

use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerInterface;

class MyService implements LoggerAwareInterface
{

    protected $logger;

    public function setLogger(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }

    public function hello()
    {
        $this->logger->alert("Hello, world!");
    }

}

демо

ninsuo:runner alain$ php test.php
ninsuo:runner alain$ cat test.log
[2014-11-06 08:18:55] my.service.ALERT: Hello, world! [] []

Вы можете попробовать это

<?php

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


class Loggr{

    private static $_logger;
    public $_instance;
    public $_channel;

    private function __construct(){
        if(!isset(self::$_logger))
            self::$_logger = new Logger('Application Log');
    }

    // Create the logger
    public function logError($error){
            self::$_logger->pushHandler(new StreamHandler(LOG_PATH . 'application.'. $this->_channel . '.log', Logger::ERROR));
            self::$_logger->addError($error);
    }

    public function logInfo($info){
            self::$_logger->pushHandler(new StreamHandler(LOG_PATH . 'application.'. $this->_channel . '.log', Logger::INFO));
            self::$_logger->addInfo($info);
    }

    public static function getInstance($channel) {

        $_instance = new Loggr();
        $_instance->_channel = strtolower($channel);        

        return $_instance;
    }
}

И может потребляться как

class LeadReport extends Controller{

    public function __construct(){

        $this->logger = Loggr::getInstance('cron');
        $this->logger->logError('Error generating leads');

    }
}