Introduction

This article deals with one of the most versatile tools used when writing code – loggers. Apart from explaining how to create your own log handler in your separated files, we’ll have a look at how they work. 

Why (and what) to log? 

The purpose of logging events occuring when running code is the ability to go back to whatever happened and analyze the executed code, step by step. It allows you to find an error that happened a few days ago but hasn’t been reported until now. Logging can also notify you instantly whenever a critical error occurs. 

Logs are valuable sources of information. Take system logs or unsuccessful SSH login attempts logs, not stricte Magento cases. They help us keep track of past events or ones we’re not able to control real-time. 

You’ve probably seen Magento logs, especially those in the var/log/system.log or var/log/exception.log files. In production mode, there are also reports (var/report) that are generated to hide an error on the frontend and prevent a page or its part to display for a user. 

LoggerInterface and Monolog

You could’ve also noticed – whether in articles or directly in code – that to use the logger, you must inject the \Psr\Log\LoggerInterface interface. PSR stands for PHP Standard Recommendation and it’s been manufactured by PHP Framework Interop Group (PHP-FIG). The logger is derived from PSR-3 that describes the logger construction in PHP applications: PSR-3: Logger Interface – PHP-FIG.

PHP-FIG released an official package containing the definition of interfaces: GitHub – php-fig/log. The LoggerInterface itself contains a declaration of public methods (compliant with PSR-3) that should be included in the interface implementation. Thanks to this, we can also be sure we can log through the correct usage of methods that mirror the logged information (like debug, info, warning, critical, etc.). It doesn’t matter what exact logger is used in the application. 

Concluding, in Magento, injecting an interface to the class will result in using a specific implementation, \Magento\Framework\Logger\Monolog, which in turn is a simple extension of \Monolog\Logger\Logger. On another note, the Magento extension of Monolog allows fetching messages from Exception, if it has been passed to the logger. 

Every type of log (debug, info, etc.) has its value, distinguished by its handler. A logger choses a proper handler based on the log type and processes the log in the right class. Below you will find a list of available log types along with their properties and short descriptions. 

// file: vendor/monolog/monolog/src/Monolog/Logger.php
    /**
     * Detailed debug information
     */
    const DEBUG = 100;

    /**
     * Interesting events
     *
     * Examples: User logs in, SQL logs.
     */
    const INFO = 200;

    /**
     * Uncommon events
     */
    const NOTICE = 250;

    /**

     * Exceptional occurrences that are not errors
     *
     * Examples: Use of deprecated APIs, poor use of an API,
     * undesirable things that are not necessarily wrong.
     */
    const WARNING = 300;

    /**
     * Runtime errors
     */
    const ERROR = 400;

    /**
     * Critical conditions
     *
     * Example: Application component unavailable, unexpected exception.
     */
    const CRITICAL = 500;

    /**
     * Action must be taken immediately
     *
     * Example: Entire website down, database unavailable, etc.
     * This should trigger the SMS alerts and wake you up.
     */
    const ALERT = 550;

    /**
     * Urgent alert.
     */
    const EMERGENCY = 600;

Each type has its equivalent method (the aforementioned LoggerInterface). Based on the above, you can use the right methods and the logger will define how to process a given log. 

Handlers

Monolog provides us with the right methods that we should use in our code by implementing the LoggerInterface interface. It also allows using different handlers. It means we can save a log to a file, a database or other places. The class itself doesn’t know where a log is saved to – it uses the handler for that. 

Apart from implementing log processing, handlers can check if a given handler should process a specific type of log. E.g. we can save debug logs to a file and emergency logs can be sent by a text message. Also, handlers allow us to use log processors and formatters. 

Create your own logger

Once we’re done with the theoretical part, let’s say we simply want to log information from a module to a specific place. To simplify, let’s use a handler that saves logs to a file (If you’d like to see how to create a handler saving logs e.g. to a database or using API, let me know in the comments!). 

Assumptions: 

  • We want to save logs to specific files
  • Logs of the NOTICE level and below will be saved to the var/log/our_module.log file
  • Logs above the NOTICE level will be saved to the var/log/our_module_error.log file
  • Logs of the NOTICE level and below can be turned on and off at will – we don’t want the module to always log tons of information if they’re not needed. 
  • Logs above the NOTICE level will always be saved. 

I won’t be creating a module from scratch. I will use a module I wrote for the previous article (link). 

First, let’s create two classes responsible for saving logs to different places – e.g. for logs below and above the NOTICE level. 

// file: app/code/Magently/Customer/Logger/Debug.php

<?php

namespace Magently\Customer\Logger;

use Magento\Framework\Logger\Handler\Base;
use Monolog\Logger;

/**
 * Class Debug
 * The handler for storing debug messages from module
 */
class Debug extends Base
{
    /**
     * @var integer
     */
    protected $loggerType = Logger::DEBUG;

    /**
     * {@inheritDoc}
     * @param array $record
     * @return void
     */
    public function write(array $record)
    {
        // our condition here
        if (true) {
            parent::write($record);
        }
    }
}

// file: app/code/Magently/Customer/Logger/Error.php

<?php

namespace Magently\Customer\Logger;

use Magento\Framework\Logger\Handler\Base;
use Monolog\Logger;

/**
 * Class Error
 * The andler for storing errors from module
 */
class Error extends Base
{
    /**
     * @var integer
     */
    protected $loggerType = Logger::WARNING;
}

Both these classes extend \Magento\Framework\Logger\Handler\Base that has a function to save logs to files. 

The difference between the classes is pretty simple. In the Debug handler, we modify the parent method write and check if our “debug mode” is on. The Error handler only contains the modification of the $loggerType property. 

 

Important – if you look at the logic of picking a handler by the logger, you’ll find a following line: 

return $record['level'] >= $this->level;

It means that if a value (constants shown earlier) of a logged event is equal or higher than the loggerType property of the handler, the log will be processed. In our example, logging an error ($logger->error('ERROR')) will process it through both Debug and Error handlers. But if you log on INFO level ($logger->info('info')), it will be only processed by the Debug handler because INFO has a value of 200 and our Error handler only logs everything equal or higher than 300.

 

Now that we’ve defined the classes, let’s move to di.xml, where we’ll define places our log should be saved to. 

// file: app/code/Magently/Customer/etc/di.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

        xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    ...
    <!-- Logger configuration -->
    <type name="Magently\Customer\Logger\Error">
        <arguments>
            <argument name="fileName" xsi:type="string">var/log/our_module_error.log</argument>
        </arguments>
    </type>
    <type name="Magently\Customer\Logger\Debug">
        <arguments>
            <argument name="fileName" xsi:type="string">var/log/our_module.log</argument>
        </arguments>
    </type>

    <virtualType name="ourLogger" type="Magento\Framework\Logger\Monolog">
        <arguments>
            <argument name="handlers" xsi:type="array">
                <item name="error" xsi:type="object">Magently\Customer\Logger\Error</item>
                <item name="debug" xsi:type="object">Magently\Customer\Logger\Debug</item>
            </argument>
        </arguments>
    </virtualType>
    ...
</config>

 

There are two things here we need to discuss: 

Firstly, by using type we injected file names to the handlers. Secondly, we created a virtualType for the default Magento logger and we passed the array with newly created handlers to the constructor. 

And that’s so much for defining loggers. Obviously, you can inject handlers directly to the Magento logger (it’s useful if you want the alert error notifications to come via a different channel like email or a text message). 

To use the handlers, inject the virtualType to a class containing the logger: 

// file: app/code/Magently/Customer/Model/SomeClass.php

<?php

namespace Magently\Customer\Model;

use Psr\Log\LoggerInterface;

/**
 * Class SomeClass
 * The class responsible for doing something
 */
class SomeClass
{
    /**
     * @var LoggerInterface
     */
    private $logger;

    /**
     * SomeClass constructor.
     * @param LoggerInterface $logger
     */
    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }

    /**
     * Do something
     * @return void
     */
    public function execute()
    {
        $this->logger->alert('WAKE UP!');
        $this->logger->debug('Next step');
        $this->logger->notice('Take a look at this');
        $this->logger->error('Something went wrong', ['param1' => 'value1']);
    }
}
// file: app/code/Magently/Customer/etc/di.xml
    ...
    <type name="Magently\Customer\Model\SomeClass">
        <arguments>
            <argument name="logger" xsi:type="object">ourLogger</argument>
        </arguments>
    </type>
    ...

 

Note that we still inject the LoggerInterface interface in the constructor. The virtualType is built upon the Magento logger that extends the Monolog logger, which in turn implements LoggerInterface. Everything is working as intended and we’re still free to use a different logger. 

The result can be seen after executing the execute method:

$ ls var/log/our_module
our_module.log        our_module_error.log
$ cat var/log/our_module.log 
[2020-07-09 13:48:36] main.ALERT: WAKE UP! [] []
[2020-07-09 13:48:36] main.DEBUG: Next step [] []
[2020-07-09 13:48:36] main.NOTICE: Take a look at this [] []
[2020-07-09 13:48:36] main.ERROR: Something went wrong {"param1":"value1"} []

 

As you can see, all logs went to our_module.log and only the ones we wanted went to the error file: 

$ cat var/log/our_module_error.log 
[2020-07-09 13:48:36] main.ALERT: WAKE UP! [] []
[2020-07-09 13:48:36] main.ERROR: Something went wrong {"param1":"value1"} []

Summary

As you can see, there are plenty of advantages of a well-developed method of processing logs, both through recommended PSR standards and through Monolog implementation. Thanks to Dependency Injection we have a control over what handler and what logger will be used in our modules. That’s all for today. Share your thoughts and opinions in the comments!