PSR 3 logging in a PHP application - Stefanius67/XLogger GitHub Wiki

Introduction

Logging within an application or module is one of the overarching tasks that is usually required regardless of the purpose of the component. Be it to log error messages or certain events during the process runtime, or to record information during debugging.

For this purpose, appropriate code for logging is usually included in every PHP project, in most cases using an appropriate library that takes over this task.

Depending on the actual purpose of the respective logging and especially on the phase in which the current project is located, it makes sense to define the output target and format as well as the scope of the data to be logged differently.

For example, what is helpful for troubleshooting during the development phase can be superfluous or even undesirable in the delivered product (because this could result in the publication of internal technical information).

Why Use PSR-3?

The use of PSR-3 ensures the highest possible independence of the actual source code from the logging library ultimately used, since the interface is clearly defined. One advantage is that you can use a different library at any time without having to make any changes in your own source code. Just as important is the possibility that any external modules or components that implement PSR-3-compliant logging can be easily integrated into your own project and their logging is thus automatically integrated into the system's own logging. Conversely, a self-created component based on PSR-3 logging can be integrated into third-party projects much faster.

PSR-3 and the dependency inversion principle

However, using a PSR-3 compatible logging library alone does not guarantee the independence of your own source code. As a further important step, no instance of a logger should be created within its own classes, but each class should implement a method via which an object that has already been instantiated and possibly configured according to the environment and that implements the PSR-3 LoggerInterface is transferred. The easiest way to do this is to either implement your own classes using the LoggerAwareInterface, which is also included in the PSR3 specification, or, if this is not possible (if the own class already extends any other class or implements any other interface), simply integrate the LoggerAwareTrait by an use statement.

Another common pattern is to initialize the internal logger property in the constructor of your own class with an instance of the NullLogger class contained in the PSR3 specification. This class implements the Logger interface, but actually does not do anything at all. The purpose of this class is that the logger can be called inside the class without need to validate the internal logger each time.

If the loger property is not initialized with a NullLogger, the logger should be included as an argument in the class constructor to ensure that a valid logger is available.

The PSR-3 LoggerInterface

In order to log PSR3 compatible, it is necessary to examine the structure and methods of the logger interface more closely.

The interface provides 8 methods for creating log entries with the appropriate weighting. In addition, a general method is defined that receives the weighting of the entry to be created as the first parameter.

PSR3 defines the following levels based on RFC 5424 (defined in class LogLevel):

Level Description
Emergency the system is unusable
Alert immediate action is required
Critical critical conditions
Error errors that do not require immediate attention but should be monitored
Warning unusual or undesirable occurrences that are not errors
Notice normal but significant events
Info interesting events
Debug detailed information for debugging purposes

All methods expect the message to be logged as the first argument. This must either be a string or an object that implements the __toString() method. In addition to plain text, the message can also contain placeholders in curly braces ({key}), which are replaced by the corresponding values from the context data, which can be passed as a second argument.

The context data in the second (optional) argument are passed as an associative array. Any additional information that is not necessarily available as a string can be transferred via this array. The presentation of this context data is in the responsibility of the implementation of the logger, whereby the context data should be treated by the logger as tolerantly as possible. The content of the context data will under no circumstances lead to any exception, error or warning.

Exceptions are a special case when transferring the context data. An exception can also be passed directly in the message argument (this is permitted since all exceptions implement the __toString() method), but it is better to provide this in the context array in the key 'exception'. The implementation of the logger have thus the option to include additional, more extensive information (such as the stack trace) in the log, provided the respective output medium allows it.

Example how to implement PSR-3 logging

Below you see an example, how to implement an own class using PSR-3 logging. It shows the use of the context argument for some data and how to set an exception.

use Psr\Log\LoggerAwareTrait;
use Psr\Log\NullLogger;

/**
 * Testclass to demonstrate the integration of any PSR-3 logger into 
 * own classes. Either implement LoggerAwareInterface or integrate 
 * LoggerAwareTrait by use statement (like i do here).
 */
class TestClass
{
    use LoggerAwareTrait;

    /**
     * it makes sense to initialize the $logger property with an
     * instance of the PSR-3 NullLogger() so nowhere in the code have 
     * to be tested, if any logger is set.
     */
    public function __construct()
    {
        $this->logger = new NullLogger();
    }

    public function doSomething()
    {
        $this->logger->info('Start {class}::doSomething()', ['class' => get_class($this)]);
        for ($i = 1; $i < 10; $i++) {
            // run a loop
            $this->logger->debug('Run loop ({loop})', ['loop' => $i]);
        }
        $this->logger->info('{class}::doSomething() finished', ['class' => get_class($this)]);
    }

    public function causeException()
    {
        try {
            $this->throwException();
        } catch (Exception $e) {
            $this->logger->error('Catch Exception', ['exception' => $e]);
        }
    }

    protected function throwException()
    {
        throw new Exception('this is an Exception');
    }
}

With such a design, we are now able to use the class and control the logging behavior without having to change the class itself. We can use any PSR-3 compliant logger depending on requirements or environment.

$oTest = new TestClass();

// just work without logging...
$oTest->doSomething();

// work with our own PSR-3 logger
$myLogger = new MyPSR3Logger(LogLevel::WARNING);
$oTest->setLogger($myLogger);
$oTest->doSomething();

// ... or use i.e. a Monolog logger
$logger = new MonologLogger('Test');
$logger->pushHandler(new MonologHandlerStreamHandler('test.log'));
$oTest->setLogger($logger);
$oTest->doSomething();