9

I would like to implement logging mechanism to file in PHP:

  1. log file path will be in config file config.php
  2. in several classes I would like to log some events into the log file

For example:

    Class A {

        public function f_A {
            log_to_file($message);
        }

    }

    Class B {

        public function f_B {
            log_to_file($message);
        }

    }

I will be very grateful for any tips. I would like to implement some easy and elegant solution.

I was thinking about it (thank you for your answers) and I think I will do it this way (maybe, there are some errors, I was writing it from scratch):

interface Logger {
    public function log_message($message);
}

class LoggerFile implements Logger {
    private $log_file;

public function __construct($log_file) {
    $this->log_file = $log_file;
}
public function log_message($message) {
        if (is_string($message)) {
            file_put_contents($this->log_file, date("Y-m-d H:i:s")." ".$message."\n", FILE_APPEND);
        }
    }
}

//maybe in the future logging into database

class LoggerDb implements Logger {
    private $db;

    public function __construct($db) {
        //some code
    }
public function log_message($message) {
        //some code
    }
}

Class A {
    private $logger;

public function __construct(Logger $l) {
        $this->logger = $l;
    }


public function f_A {
    $this->logger->log_message($message);
}
}

Class B {
    private $logger;

public function __construct(Logger $l) {
        $this->logger = $l;
    }


public function f_B {
    $this->logger->log_message($message);
}
}

//usage:

//in config.php:

define("CONFIG_LOG_FILE", "log/app_log.log");

//in the index.php or some other files

$logger = new LoggerFile(CONFIG_LOG_FILE);

$instance_a = new A($logger);
$instance_b = new B($logger);
teo
  • 801
  • 6
  • 14

3 Answers3

24

Where are loggers used?

In general there are two major use-cases for use of loggers within your code:

  • invasive logging:

    For the most part people use this approach because it is the easiest to understand.

    In reality you should only use invasive logging if logging is part of the domain logic itself. For example - in classes that deal with payments or management of sensitive information.

  • Non-invasive logging:

    With this method instead of altering the class that you wish to log, you wrap an existing instance in a container that lets you track every exchange between instance and rest of application.

    You also gain the ability to enable such logging temporarily, while debugging some specific problem outside of the development environment or when you are conducting some research of user behaviour. Since the class of the logged instance is never altered, the risk of disrupting the project's behaviour is a lot lower when compared to invasive logging.

Implementing an invasive logger

To do this you have two main approaches available. You can either inject an instance that implements the Logger interface, or provide the class with a factory that in turn will initialize the logging system only when necessary.

Note:
Since it seems that direct injection is not some hidden mystery for you, I will leave that part out... only I would urge you to avoid using constants outside of a file where they have been defined.

Now .. the implementation with factory and lazy loading.

You start by defining the API that you will use (in perfect world you start with unit-tests).

class Foobar 
{
    private $loggerFactory;

    public function __construct(Creator $loggerFactory, ....)
    {
        $this->loggerFactory = $loggerFactory;
        ....
    }
    .... 

    public function someLoggedMethod()
    {
        $logger = $this->loggerFactory->provide('simple');
        $logger->log( ... logged data .. );
        ....
    }
    ....
}

This factory will have two additional benefits:

  • it can ensure that only one instance is created without a need for global state
  • provide a seam for use when writing unit-tests

Note:
Actually, when written this way the class Foobar only depends on an instance that implements the Creator interface. Usually you will inject either a builder (if you need to type of instance, probably with some setting) or a factory (if you want to create different instance with same interface).

Next step would be implementation of the factory:

class LazyLoggerFactory implements Creator
{

    private $loggers = [];
    private $providers = [];

    public function addProvider($name, callable $provider)
    {
        $this->providers[$name] = $provider;
        return $this;
    }

    public function provide($name)
    {
        if (array_key_exists($name, $this->loggers) === false)
        {
            $this->loggers[$name] = call_user_func($this->providers[$name]);
        }
        return $this->loggers[$name];
    }

}

When you call $factory->provide('thing');, the factory looks up if the instance has already been created. If the search fails it creates a new instance.

Note: I am actually not entirely sure that this can be called "factory" since the instantiation is really encapsulated in the anonymous functions.

And the last step is actually wiring it all up with providers:

$config = include '/path/to/config/loggers.php';

$loggerFactory = new LazyLoggerFactory;
$loggerFactory->addProvider('simple', function() use ($config){
    $instance = new SimpleFileLogger($config['log_file']);
    return $instance;
});

/* 
$loggerFactory->addProvider('fake', function(){
    $instance = new NullLogger;
    return $instance;
});
*/

$test = new Foobar( $loggerFactory );

Of course to fully understand this approach you will have to know how closures work in PHP, but you will have to learn them anyway.

Implementing non-invasive logging

The core idea of this approach is that instead of injecting the logger, you put an existing instance in a container which acts as membrane between said instance and application. This membrane can then perform different tasks, one of those is logging.

class LogBrane
{
    protected $target = null;
    protected $logger = null;

    public function __construct( $target, Logger $logger )
    {
        $this->target = $target;
        $this->logger = $logger;
    }

    public function __call( $method, $arguments )
    {
        if ( method_exists( $this->target, $method ) === false )
        {
            // sometime you will want to log call of nonexistent method
        }

        try
        {
            $response = call_user_func_array( [$this->target, $method], 
                                              $arguments );

            // write log, if you want
            $this->logger->log(....);
        }
        catch (Exception $e)
        {
            // write log about exception 
            $this->logger->log(....);

            // and re-throw to not disrupt the behavior
            throw $e;
        }
    }
}

This class can also be used together with the above described lazy factory.

To use this structure, you simply do the following:

$instance = new Foobar;

$instance = new LogBrane( $instance, $logger );
$instance->someMethod();

At this point the container which wraps the instance becomes a fully functional replacement of the original. The rest of your application can handle it as if it is a simple object (pass around, call methods upon). And the wrapped instance itself is not aware that it is being logged.

And if at some point you decide to remove the logging then it can be done without rewriting the rest of your application.

tereško
  • 58,060
  • 25
  • 98
  • 150
  • 5
    Never heard or seen non-invasive logging before... but I love the abstraction. Thee way Foobar can be logged, but with ANY logging code inside what so ever! Brilliant! – AlexMorley-Finch Dec 19 '13 at 12:56
  • @AlexMorley-Finch tnx. Good to hear that someone actually finds this interesting. – tereško Dec 20 '13 at 08:46
  • On non-invasive what you did is decorated the class with logging, but you can not log intermediate results. Also you have to decorate each class separately. – Imre L Jan 02 '14 at 18:52
  • @ImreL how did you come to the conclusions that one has to decorate each class separately? And you seem to be confusing "logging" and "debugging". If you need to log the intermediate results, then you probably should take a hard look at your code. There might be some serious *code smells*. – tereško Jan 02 '14 at 19:04
  • 1
    Simple use case: Reading config.ini, some values are out of range and for them I am falling back to defaults but i still want to log the invalid values. This kind of "debugging" is needed in real life. How could this be done in non-intrusive way? – Imre L Jan 02 '14 at 19:54
  • Loggers are **NOT** a debugging tool. That's why we have XDebug, `var_dump( $param )` and `error_log(json_encode( $param ))`; – tereško Jan 02 '14 at 19:58
  • Nice answer. I want to add, that there is PSR-3 standart developed https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md. It describes a common interface for logging libraries. – Ziumin Apr 27 '14 at 08:34
  • 1
    PSR-3 is not only pointless and misguided but also incompatible with this answer – tereško Apr 27 '14 at 08:45
  • 1
    @Ziumin .. well ... I guess I have to elaborate. PSR (PHP Specification Request) is a misnomer, because it is produced by PHP-FIG (Framework Interoperability Group). Their goal is to create common practices (application structure, interfaces, coding standards) **for frameworks that are part of this group**. The PSR-3 itself is inconsistent: it defines verbs (debug, log), adjectives (critical) and nouns (emergency, alert, notice) as method names. Basically, the whole thing seems rushed and probably will have an update (just like it happened with PSR-0, which they tried to shove in PHP5.5 core). – tereško Apr 28 '14 at 21:06
  • Seems to be complicated, and excessive. You are basically creating a new layer between each class just to log, adding the complexity to the parent class. This not only makes it more complex, but much harder to really debug things. Look at Java loggers if you want elegance, no need to use DI everywhere, sometimes it just gets in the way, and abstraction for the sake of abstraction is bad coding. – Steve Cook Aug 11 '16 at 22:15
  • 2
    @user3225313 thanks for this great input, but you either don't know what DI is or you don't understand how loggers in java actually work. Only loggers in java, that do not employ one of two methods described above, are using static classes. But then again ... you probably don't even understand why using static classes is a bad thing. – tereško Aug 12 '16 at 08:06
  • @tereško great answer, the non-invasive approach in particular is very interesting. How would you apply that in case of static methods? – Gruber Feb 21 '20 at 02:02
  • 1
    @Gruber you can't apply to static code, because the decorator requires objects. – tereško Feb 21 '20 at 16:20
1

Logger's goal is to save debugging info. Logger has to be class with interface to store message and level of distress. Implementation is secondary. Today you want file logging. Tomorrow you may want put logs to database. So that logic must be written on logger class side. There is already written nice logger called Monolog https://github.com/Seldaek/monolog

kryoz
  • 87
  • 4
  • I will do it this way - see edit of my question. I think it will be reusable and extensible in future and it is important how you wrote. – teo Sep 07 '13 at 18:58
1

If you want a full logging framework, with support for logging to different outputs, log4PHP is an open source solution.

If you want a small implementation that suits your needs now, something like this should do it

class Logger
{
    const INFO = 'info';
    const ERROR = 'error';

    private static $instance;
    private $config = array();

    private function __construct()
    {
        $this->config = require "/path/to/config.php";
    }

    private static function getInstance()
    {
        if(!self::$instance)
        {
            self::$instance = new Logger();
        }
        return self::$instance;
    }

    private function writeToFile($message)
    {
        file_put_contents($this->config['log_file'], "$message\n", FILE_APPEND);
    }

    public static function log($message, $level = Logger::INFO)
    {
        $date = date('Y-m-d H:i:s');
        $severity = "[$level]";
        $message = "$date $severity ::$message";
        self::getInstance()->writeToFile($message);
    }
}

//config.php
return array(
    'log_file' => '/tmp/my_log.txt'
);

Logger::log($message);

Not tested, but should work.

hjellek
  • 108
  • 2
  • So, would you prefer global functions? Or actually instantiating the logger class with configuration every time you want to use it? – hjellek Sep 07 '13 at 15:54
  • 2
    Have you even heard about dependency injection? – tereško Sep 07 '13 at 15:59
  • I was thinking about it. I will do it this way - see edit of my question. To tereško: my solution uses dependency injection. Thanks. – teo Sep 07 '13 at 18:56
  • 3
    http://www.ibm.com/developerworks/library/co-single/ I'm not sure who in the PHP world though DI is the only way to do things, but it is insanely myopic. Loggers are the perfect examples of the best place to use singletons to reduce complex code and make your application more efficient. No wonder I was able to improve PHP applications speeds so easily, everyone is stuck in the same ignorance of Only one way to do things.

    Guess what the creator of PHP stated frameworks bad, but he must be an idiot right?

    – Steve Cook Aug 11 '16 at 22:16
  • Sorry the post before was so ugly, I was trying to figure out comments on the site. – Steve Cook Aug 11 '16 at 22:22