Logging plays an essential part in leaving an audit trail of what events take place in a CMS. Monolog is a logging library that ships with Craft CMS 4, which can be customised to make logs easier to read and parse when used in plugins and modules.

Needle in haystack

Craft 4 replaced Yii’s log targets with Monolog, a logging library that, among other things, produces machine-friendly logs, implements PSR3 and can send logs to multiple targets (files, sockets, inboxes, databases, web services). This pull request explains the motivation as well as some of the resulting changes to how logging works in Craft.

One useful change, for example, is that logs are rotated daily (via the RotatingFileHandler class), so that the resulting log filenames include a timestamp by default, for example, web-2022-07-12.log. This makes it easier to find events that happened at specific times, over the previous naming convention (web.log, web.log.1, web.log.2, etc.).

Logs listing

If the CRAFT_STREAM_LOG PHP constant is set to true then Craft will send log output to stderr and stdout, instead of to log files.

Adding logging to a custom plugin or module in Craft 4 requires the same syntax as it did in Craft 3.

Craft::getLogger()->log($message, Logger::LEVEL_INFO);

The passed-in message is logged at the provided message level (see available message levels) to the application category by default. This will be logged to either the web, console or queue log target, depending on what kind of request this is.

One thing I’ve always found helpful, however, is being able to log messages to a dedicated log target in our plugin and is the reason we created and used the Log To File Helper for all of our Craft 3 plugins.

With Monolog at our disposal, however, we no longer need this package. We can instead create a custom log target, assign a category” to it and add it to the log message dispatcher. Then, any time we specify our category in a log, our custom log target will be used in addition to the log targets already registered by Craft. Read more about how message filtering works.

Here is the method we generally use to register custom log targets in our plugins.

use Craft;
use craft\base\Plugin;
use craft\log\MonologTarget;
use Monolog\Formatter\LineFormatter;
use Psr\Log\LogLevel;

class MyPlugin extends Plugin
{
    public function init(): void
    {
        parent::init();
        
        // Register a custom log target, keeping the format as simple as possible.
        Craft::getLogger()->dispatcher->targets[] = new MonologTarget([
            'name' => 'my-plugin-handle',
            'categories' => ['my-plugin-handle'],
            'level' => LogLevel::INFO,
            'logContext' => false,
            'allowLineBreaks' => false,
            'formatter' => new LineFormatter(
                format: "%datetime% %message%\n",
                dateFormat: 'Y-m-d H:i:s',
            ),
        ]);
    }
}

What we’re doing in the log target above is:

  • Setting the name and category to our plugin’s handle, to make the log file easy to identify.
  • Setting the log level to info, so that we only log informative events rather than low-level debug info (see Monolog’s log levels).
  • Disabling logging of the request context, so we only log the message itself.
  • Disallowing line breaks so that we only have one line per log entry.
  • Setting the format to just a timestamp, the message and a new line character.

We can then log a message using our custom log target by specifying the category in the third parameter.

use yii\log\Logger;

Craft::getLogger()->log($message, Logger::LEVEL_INFO, 'my-plugin-handle');

Or we can simply use one of the convenience methods that the Craft class provides.

// Log an informational message.
Craft::log($message, 'my-plugin-handle');

// Log an error message.
Craft::error($message, 'my-plugin-handle');

This results in the following line being written to a log file named my-plugin-handle-2022-07-12.log.

2022-07-12 12:00:00 Something magical happened.

The message will additionally be written to web-2022-07-12.log (assuming a web request), so you’ll still be able to track down the full context if necessary, as follows.

2022-07-12 12:00 [web.INFO] [my-plugin-handle] Something magical happened. {"memory":6320184} 

How we decide to format our message is completely up to us. We’ve set it to be as simple as possible, just a timestamp, the message and a new line character, which makes it human-readable and extremely terse. You can, of course, configure it however you like – see the LineFormatter class for more available options. 

Craft’s MonologTarget class is what we created a custom instance of. One thing you might notice is that its maxFiles property is set to 5 by default, meaning that at most 5 log files will be kept in rotation for each category”. If you want to maintain logs for longer periods of time then it makes sense to increase this value.

In order to keep things organised and DRY, it is possible to refactor each of the pieces above into their own methods as follows.

use Craft;
use craft\base\Plugin;
use craft\log\MonologTarget;
use Monolog\Formatter\LineFormatter;
use Psr\Log\LogLevel;
use yii\log\Logger;

class MyPlugin extends Plugin
{
    public function init(): void
    {
        parent::init();
        
        $this->_registerLogTarget();
    }
    
    /**
     * Logs an informational message to our custom log target.
     */
    public static function info(string $message): void
    {
        Craft::info($message, 'my-plugin-handle');
    }
    
    /**
     * Logs an error message to our custom log target.
     */
    public static function error(string $message): void
    {
        Craft::error($message, 'my-plugin-handle');
    }
    
    /**
     * Registers a custom log target, keeping the format as simple as possible.
     */
    private function _registerLogTarget(): void
    {
        Craft::getLogger()->dispatcher->targets[] = new MonologTarget([
            'name' => 'my-plugin-handle',
            'categories' => ['my-plugin-handle'],
            'level' => LogLevel::INFO,
            'logContext' => false,
            'allowLineBreaks' => false,
            'formatter' => new LineFormatter(
                format: "%datetime% %message%\n",
                dateFormat: 'Y-m-d H:i:s',
            ),
        ]);
    }
}

We can then log messages using our plugin’s log() and error() methods.

// Log an informational message.
MyPlugin::info($message);

// Log an error message.
MyPlugin::error($message);

There is so much more you can do with Monolog but this article should help get you started with custom logging in your plugins and modules. For more options, I’ll refer you to the Monolog docs.