Implementing PSR-3 with log4php

With the recent passage of PSR-3, a standard for a common interface for logging libraries, it seems appropriate to discuss the implementation with my favorite logging library. log4php is an open source Apache project that is a very versatile logging framework.

Through log4php’s configuration files, logging has a variety of output locations, so it’s possible to send specific log levels to different output locations. For example: debug, info, and notice are used mainly for general logging and debugging, and so it makes sense to direct these to a standard log file, whereas critical and above typically mean something has gone horribly wrong with our application and it makes more sense to send these to ourselves via email for immediate diagnosis.

Getting Dependencies

Before we can start writing code, we need to our dependencies. The PSR-3 interface class is required; it’s currently found on GitHub and can be installed via composer from Packagist. Also, the log4php framework is required, which can be downloaded from Apache, with PEAR, or composer.

Here’s a sample composer.json file that sets everything up:

{
    "require": {
        "psr/log": "dev-master",
        "apache/log4php": "2.3.0"
    }
}

Wrapping log4php

With the necessary third-party libraries installed, we can create a generic logging class which implements the PSR-3 interface and uses log4php.

Eight different log levels are exposed by the PSR-3 interface: debug, info, notice, warning, error, critical, alert, and emergency. log4php contains six logging levels: trace, debug, info, warn, error, and fatal. We have to map the PSR-3 levels to appropriate log4php levels. The below class implements interface and manages a log4php logger instance to map the levels.

<?php
require_once 'vendor/autoload.php';

class MyLogger implements PsrLogLoggerInterface
{
    private $logger;

    public function __construct($logger = 'main', $config = null) {
        Logger::configure($config);
        $this->logger = Logger::getLogger($logger);
    }

    /**
     * System is unusable.
     *
     * @param string $message
     * @param array $context
     * @return null
     */
    public function emergency($message, array $context = array()) {
        $this->logger->fatal($this->interpolate($message, $context));
    }

    /**
     * Action must be taken immediately.
     *
     * @param string $message
     * @param array $context
     * @return null
     */
    public function alert($message, array $context = array()) {
       $this->logger->fatal($this->interpolate($message, $context));
    }

    /**
     * Critical conditions.
     *
     * @param string $message
     * @param array $context
     * @return null
     */
    public function critical($message, array $context = array()) {
        $this->logger->fatal($this->interpolate($message, $context));
    }

    /**
     * Runtime errors that do not require immediate action but should
     * be logged and monitored.
     *
     * @param string $message
     * @param array $context
     * @return null
     */
    public function error($message, array $context = array()) {
        $this->logger->error($this->interpolate($message, $context));
    }

    /**
     * Exceptional occurrences that are not errors.
     *
     * @param string $message
     * @param array $context
     * @return null
     */
    public function warning($message, array $context = array()) {
        $this->logger->warn($this->interpolate($message, $context));
    }
    /**
     * Normal but significant events.
     *
     * @param string $message
     * @param array $context
     * @return null
     */
    public function notice($message, array $context = array()) {
        $this->logger->info($this->interpolate($message, $context));
    }

    /**
     * Interesting events.
     *
     * @param string $message
     * @param array $context
     * @return null
     */
    public function info($message, array $context = array()) {
        $this->logger->info($this->interpolate($message, $context));
    }

    /**
     * Detailed debug information.
     *
     * @param string $message
     * @param array $context
     * @return null
     */
    public function debug($message, array $context = array()) {
        $this->logger->debug($this->interpolate($message, $context));
    }

    /**
     * Logs with an arbitrary level.
     *
     * @param mixed $level
     * @param string $message
     * @param array $context
     * @return null
     */
    public function log($level, $message, array $context = array()) {
        throw new Exception('Please call specific logging message');
    }

    /**
     * Interpolates context values into the message placeholders.
     * Taken from PSR-3's example implementation.
     */
    protected function interpolate($message, array $context = array()) {
        // build a replacement array with braces around the context
        // keys
        $replace = array();
        foreach ($context as $key => $val) {
            $replace['{' . $key . '}'] = $val;
        }

        // interpolate replacement values into the message and return
        return strtr($message, $replace);
    }
}

Apart from implementing the PSR-3 interface, this class contains a constructor that accepts 2 optional parameters named $logger and $config. The first parameter allows you to specify a name for the logger and log4php will maintain a reference to it allowing you to maintain multiple logger instances at the same time with different names. The second parameter allows you to specify a configuration file. If none is specified, log4php will use its default configuration.

The newly created MyLogger class can now be tested with the following example:

<?php
$logger = new MyLogger();
$logger->debug('My debug test');

The default log4php setting for debug will output the message to standard out, so the results of this example will be sent to the web browser.

log4php Configuration

log4php ships with a default configuration so we get started using it right away, but it also provides the ability to override the configuration using XML or PHP code. The most popular supported configuration is XML.

Let’s create a config.xml file with a new appender that will tell log4php to log all messages with warn and above to a file:

<configuration xmlns="http://logging.apache.org/log4php/">
 <appender name="myAppender" class="LoggerAppenderFile">
  <param name="file" value="myLog.log"/>
 </appender>
 <root>
  <level value="WARN"/>
   <appender_ref ref="myAppender"/>
  </root>
</configuration>

We now need to pass the name of the new configuration file to MyLogger when we invoke its constructor.

<?php
$logger = new MyLogger('main', 'config.xml');
$logger->debug('My debug test');

If you re-run the example, when the debug message is called, log4php will ignore it because debug is below warn and our configuration only logs warn and above.

Conclusion

By leveraging the PSR-3 logging interface and log4php, standards-compliant logging is now extremely simple to add to any of our projects. To learn more about log4php, visit the Quick Start guide. For more about PSR-3, read the standard on GitHub.

Image via Fotolia

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • http://szjani.hu szjani

    I think you shouldn’t pass the configuration to the constructor and call Logger::configure() because every time you instantiate MyLogger, log4php will be reconfigured.

  • Patrick

    Maybe you should try actually reading the PSR-3 spec?

    “A ninth method, log, accepts a log level as first argument. Calling this method with one of the log level constants MUST have the same result as calling the level-specific method.”

    Your code here violates this rule – calling the log() method results in an exception, not the same behavior as the level-specific methods. A better idea would be to extend the AbstractLogger provided as part of PSR-3, and use a switch statement within the log() method to do the mapping.

    Your code is not PSR-3 compliant.

    It’s also just bad code generally, as you’re hard-coding your dependencies and creating tight coupling with the Logger object, making your code essentially impossible to unit test.

  • http://seld.be/ Jordi Boggiano

    Patrick is right that the log() method should accept messages too to be compliant.

    By the way for anyone interested in using a PSR-3 logger without having to write a wrapper yourself, you can try any 1.3+ release of Monolog, which is now PSR-3 compliant: https://github.com/Seldaek/monolog (or require monolog/monolog via composer). I understand the goal of the article was how to write one yourself, but while it can be a good exercise I don’t think everyone should have to do this.

  • Daniel Ribeiro

    Totally agree with Pattrick on this one.

    This article hurts the SOLID principles of software development, and the code shown is not PSR-3 compliant, like Pattric mentioned.

  • http://szjani.hu szjani

    What was the problem with my comment?

  • Ivan Habunek

    Apache log4php dev here. Nice to see you already integrating things. We aim to become compliant soon.

  • http://zaemis.blogspot.com Timothy Boronczyk

    Sometimes things don’t go according to our intentions. When I saw that PSR3 was accepted, I commissioned Jamie to write an article that included it in his content. The goal wasn’t to be first to publish an article on the interface (so no, this wasn’t a minor repeat of the PHP6 fiasco we saw in the publishing industry a few years back), rather, it was to continue to provide relevant, interesting, and timely articles for the PHP community. I had an open publishing slot, the author had a free weekend, and so the article was written. Unfortunately, some have taken issue with the quality of the example code presented, and others argue that the code doesn’t implement PSR3.

    This discussion has gone beyond PHPMaster into other arenas. Some have presented their issues in a troll-like manner. Others have presented their issues with the article in a more professional way, and of that I’m very appreciative. 100% quality, just like 100% up-time, is practically impossible. Its important to remember that we’re all human and make mistakes.

    The PHP community has been growing by leaps and bounds since it’s found acceptance in the corporate sector and Ruby has come on the scene to challenge it. We’ve seen renewed interest in initiatives like PHP-FIG, the appearance of tools like Composer, and so on. One can’t be an expert in all of them and I often rely on the expertise of the author. But with that said, I am the one who should and will accept responsibility for releasing an inaccurate article.

    As managing editor, I find myself both an advocate and gate keeper. I do my best to spend time with each author to help them not only hone their writing, but also grow as a programmer. Jamie and I briefly discussed the SOLID principles in his example code, but in the end I felt it acceptable to encapsulate the logger instance given the nature of the wrapper. I justified bad code and then pushed it on to be published. The blame is mine.

    Together, we all strive to produce the best possible articles within the constraints we find ourselves in. Mistakes will be made, and along the way seemingly innocent decisions will come back on us, but regardless, we pick ourselves up and carry on, learning as best we can.