1

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! [] []
Alain Tiemblo
  • 36,099
  • 17
  • 121
  • 153

3 Answers3

2

What would you think about making the decision which logger should be used right before the consumers are created? This could be easily accomplished with some kind of DIC or maybe a factory.

<?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));
Markus
  • 693
  • 5
  • 13
  • Thanks for your idea. I will post an answer too (I chosen to override the service Container, it automatically injects a logger to LoggerAware services), but I awards you the bounty anyway. – Alain Tiemblo Oct 30 '14 at 10:38
0

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! [] []
Alain Tiemblo
  • 36,099
  • 17
  • 121
  • 153
0

You can try this

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

and can be consumed as

class LeadReport extends Controller{

    public function __construct(){

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

    }
}
Moghira
  • 317
  • 5
  • 10