phploggingmonologsymfony-components

How to set-up a generic leveraged logging using Monolog?


I am writting a console application with Symfony2 components, and I want to add distinct logging channels for my services, my commands and so on. The problem: to create a new channel requires to create a new instance of Monolog, and I don't really know how to handle this in a generic way, and without needing to pass the stream handler, a channel and the proper code to bind the one and the other inside all services.

I did the trick using 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);
}

Whatever from where I call my logger, it creates a channel for each class that called it. Looks cool, but as soon as a logger is called tons of time, this is performance-killing.

So here is my question:

Do you know a better generic way to create one distinct monolog channel per class that have a logger property?


The above code packaged for testing:

composer.json

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

test.php

<?php

require('vendor/autoload.php');

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

class Test
{

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

}

class Hello
{

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

}

class LeveragedLogger implements \Psr\Log\LoggerInterface
{

    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);

Usage

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

Solution

  • I finally created a MonologContainer class that extends the standard Symfony2 container, and injects a Logger to LoggerAware services. Overloading the get() method of the service container, I can get the service's ID, and use it as a channel for the logger.

    <?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;
        }
    
    }
    

    Usage example:

    test.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();
    

    services.yml

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

    MyService.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!");
        }
    
    }
    

    Demo

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